0%

这一章介绍的大都是用来查询类型属性的 type traits。还有一些其他的工具。

按数据概念分类

下面是基本(primary)类型,范围都是互斥的,而且任何一个类型必然使得下面中的一个且仅有一个评估为真:

(C++14 之前没有 is_null_pointer,所以那个时候 nullptr 的类型 std:: nullptr_t 是个特例。)

定义和使用 concept

下面演示了几种 C++20 支持的定义 concept 的方式,每一条约束都是以下之一,然后用分号结尾:

  1. 类型
  2. 表达式
  3. { 表达式 } noexcept -> 检查返回类型是否满足其他约束(这个 noexcept 可以不要)
  4. 引入其他 requires 表达式
template <typename T>
concept StringConcept = requires(T t) {
    typename T::iterator; // 1. 检查类型存在性
    t.data();             // 2. 检查表达式合法性

    // 3. 将表达式作为 concept 的第一个参数,要求 concept 成立(额外参数从第二个参数起放置)
    { t.c_str() } -> std::same_as<char const *>;

    // 4. 用 requires 语句引入子条件
    requires std::is_pointer_v<decltype(t.data())>;

    // 5. 除了 requires 体之外还能用其他编译期常量表达式做约束
} && (sizeof(T) > 8) && !std::is_aggregate_v<T>;

template<StringConcept Str>
void takeString(Str const &s) { }

int main() {
    takeString(std::string{"hello"});
}

在模板中加上 requires 条件则相对比较简单。只需要在 模板参数后,函数返回值前面,或者 函数体前面 加上 requires 约束。requires 约束能使用一般的条件表达式,也能把 concept 当成 type trait 使用。比如:

template <typename T>
requires HasPlus<T>
int f(T p)
{}

template <typename T>
int g(T p)
requires HasPlus<T>
{}

// 注意 requires 约束的位置比较灵活

C 风格枚举和 C++ 新增的 enum class:

enum { ITEM_A1, ITEM_A2 };
enum B { ITEM_B1, ITEM_B2 };
// 从 C 沿用来的语法:
// 1. 底层类型默认为int,但也能手动指定
// 2. 枚举名裸露在外
// 区别:
// C 允许前向声明 enum
// C++ 允许枚举列表为空;允许在表示类型时省略 enum 关键字;虽然枚举名裸露但也能通过限定名访问

// C++ 新增 enum class
enum C { ITEM_C1, ITEM_C2 };
// 类型不再裸露,需要使用限定名访问
// 没有默认底层类型,但是可以手动指定

C++ enum class 禁止了隐式转换,但是用来表示 id 还是不太方便:

#include <type_traits>

enum A: int { ITEM_1 };
enum { ITEM_2 };
enum class B: int { ITEM_3 };

// 即便底层类型相同,实际类型也不同
static_assert(! std::is_same_v<int, A>);
static_assert(sizeof(int) == sizeof(A));
static_assert(sizeof(int) == sizeof(ITEM_2));
// 枚举不是聚合类
static_assert(! std::is_aggregate_v<A>);
static_assert(! std::is_aggregate_v<B>);

int main() {
    [](int){}(ITEM_1);
    [](int){}(ITEM_2);
    
    int b3 = (int)B::ITEM_3;
    // [](int){}(B::ITEM_3); // no conversion from B to int
    // [](B){}(b3);          // no conversion from int to B
    [](B){}(B {b3});
    // [](B){}( {b3} );      // 即便是花括号也不能推导,因为枚举不是类

    // 强类型整数还是用 aggregate 做比较好:
    // 虽然整数到枚举可以使用构造的方式描述,但枚举到整数只能强制转换
    // 作为对比,aggregate 直接访问成员即可
}

一般认为 explicit 是用在单参数构造函数 上防止隐式类型转换的。但如果参数有多个,explicit 关键字也有其他的作用。

explicit 可以阻止 {} 推导

https://stackoverflow.com/a/39122237/

可以参考另一篇文章:{ } Syntax。

struct Foo { Foo(int, int) {} };
struct Bar { explicit Bar(int, int) {} };

Foo f1(1, 1); // ok
Foo f2 {1, 1}; // ok
Foo f3 = {1, 1}; // ok

Bar b1(1, 1); // ok
Bar b2 {1, 1}; // ok
Bar b3 = {1, 1}; // NOT OKAY

Bar many_bars[] = { {1, 1} };     // ERROR explicit阻止了 {1, 1} 被推导成 Bar{1, 1}
Bar many_bars2[] = { Bar{1, 1} }; // okay

阻止“去掉默认值是单参数构造函数”的隐式转换

如果将来某个构造函数除了第一个参数之外的其他参数有了默认值,则会引起和单参数构造函数一样的隐式转换问题。

https://www.cppstories.com/2021/heterogeneous-access-cpp20/

C++ 14 加入了对有序容器的异质查找

用户的工作量很小,对标准库中的类型,只需要加上第三个模板参数:std::less<>(它的默认参数是 void)。std::less<void> 类中申明了 is_transparent 类型,所以可以用于异质查找。

std::map<std::string, int> intMap {
    { "Hello Super Long String", 1 },
    { "Another Longish String", 2 },
    { "This cannot fall into SSO buffer", 3 }
};

std::cout << "Lookup in intMap with by const char*:\\n";
std::cout << intMap.contains("Hello Super Long String") << '\\n';
//
std::map<std::string, int, std::less<>> trIntMap {
    { "Hello Super Long String", 1 },
    { "Another Longish String", 2 },
    {"This cannot fall into SSO buffer", 3 }
};

std::cout << "Lookup in trIntMap by const char*: \\n";
std::cout << trIntMap.contains("Hello Super Long String") << '\\n';

C++ 20 加入了对无序容器的异质查找

需要提供标注 is_transparent 的 hash 算子和等于算子。等于算子一般可以直接用 std::equal_to<>,但 hash 算子常常需要我们自己提供。

struct string_hash {
  using is_transparent = void;
  [[nodiscard]] size_t operator()(const char *txt) const {
    // 对指针的hash只考虑了地址,所以要选用string_view的hash
    return std::hash<std::string_view>{}(txt);
  }
  [[nodiscard]] size_t operator()(std::string_view txt) const {
    return std::hash<std::string_view>{}(txt);
  }
  [[nodiscard]] size_t operator()(const std::string &txt) const {
    // string和string_view的hash方式一样
    // 但用string的特化会减少一次从string到string_view的构造
    return std::hash<std::string>{}(txt);
  }
};

std::unordered_map<std::string, int, string_hash, std::equal_to<>>
      intMapTransparent {
    { "Hello Super Long String", 1 },
    { "Another Longish String", 2 },
    {"This cannot fall into SSO buffer", 3 }
};

bool found = intMapTransparent.contains("Hello Super Long String");
std::cout << "Found: " << std::boolalpha << found << '\\n';

下面的代码中,Bar 能够隐式转换成 Foo。想要重载 Foo 的等于运算符至少有三种方案:

  1. 重载全局的 == 运算符
  2. 重载 Foo 中的 hidden friend,即 friend bool operator==(Foo const &a, Foo const &b)
  3. 重载 bool operator==(Foo const &a) const

第一种方案会污染全局的名字空间,使得 Bar 和 Bar 之间也能通过转换两个参数进行比较;第三种方案允许第二个参数的隐式类型转换,但要求第一个参数必须是 Foo 类型;第二种方案只要求任一个参数为 Foo 类型,使得 ADL 能够参与找到这个函数,另一个参数则可以通过隐式类型转换来得到。

成员函数的劣势是由于查找方式和 this 指针的存在,函数失去了对称性。全局函数的劣势则是没有用上 ADL 的优势,hidden friends 在这种情况比较好。

https://godbolt.org/z/6sxdno3Yh

publicprotectedprivate
共有继承publicprotected不可见
私有继承privateprivate不可见
保护继承protectedprotected不可见

可见继承属性就是对于来自基类的 public 和 protected 成员进行一个取最小权限的操作(定义权限 public > protected > private)。

struct 和 class 的数据定义没有区别。语法上区别是:struct 定义默认继承种类是 public,class 定义默认继承类型是 private,这和基类用 struct 还是 class 声明的无关

继承多个类时,需要给不同的基类分别指定访问限定符。

子类继承父类的 protected 属性/方法之后,可以通过子类的指针去访问;子类也不能通过父类指针访问 protected 的成员。

这段代码是在 Tamper Monkey 的 content.js 中发现的。被浏览器警告应该替换掉这种写法。

bn.addEventListener("DOMNodeInserted", o, s)

替换成

var [adder, remover] = ((bn, o, s) => {
    var helper = { remove : () => {}, add : () => {} }
    var observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        var nodes = Array.prototype.slice.call(mutation.addedNodes);
        nodes.forEach((node_) => { o(); helper.remove() });
      });
    });
    helper.remove = () => observer.disconnect();
    helper.add = () => observer.observe(bn, { childList: true, subtree: true, };
    return [helper.add, helper.remove];
})(bn, o, s);
adder()
bn.removeEventListener("DOMNodeInserted", o, s)

C++11

虽然有了 auto 关键字,但是用起来还是需要 trailing return type 声明。

C++14

可以省略尾部声明(以下两种写法都是要 C++14 才能支持):

auto f()         { return 42; } // #1
auto f() -> auto { return 42; } // #2,相当于 #1

Note

例外:在 C++23 explicit object parameter 可用之前,构造递归 lambda 时需要显式声明返回值类型,否则无法成功推导(或者先用 function 模板类存储起来,类型也就能从 function 中推导)。

同时 C++14 还支持 generic lambda,即使用 auto 作为函数参数的类型。

$()${}

除了 $ 之外,$()${} 都是 make 替换变量的语法。但是 ${} 还能被某些 shell(比如 bash)继续替换。

此外,它们也能调用 make 提供的命令,比如字符串替换、过滤和调用 shell。

Shell 函数无法正常传递 * 参数

是因为 make 传参的时候将引号去掉了,导致星号又被 shell 解释了一次(我用的 MSYS2)。

$ cat Makefile
all:
        echo \'*\'
$ make
echo \'*\'
'*'
$ nvim Makefile
$ cat Makefile
all:
        echo '*'
$ make
echo '*'
Makefile

兼容性

  • 使用 ./ 来表示路径,不要使用 .\。(但是这样 cmd 解释不了,得用 bash 才行)
  • 使用命令名来调用,不要使用 .exe 后缀。