0%
CTTCG 附录 E Concept
定义和使用 concept
下面演示了几种 C++20 支持的定义 concept 的方式,每一条约束都是以下之一,然后用分号结尾:
- 类型
- 表达式
- { 表达式 } noexcept -> 检查返回类型是否满足其他约束(这个 noexcept 可以不要)
- 引入其他
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 约束的位置比较灵活
enum class
发表于:
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 关键字也有其他的作用。
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
阻止“去掉默认值是单参数构造函数”的隐式转换
如果将来某个构造函数除了第一个参数之外的其他参数有了默认值,则会引起和单参数构造函数一样的隐式转换问题。
Heterogeneous Lookup
发表于:
更新于:
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';
Hidden Friends
发表于:
下面的代码中,Bar 能够隐式转换成 Foo。想要重载 Foo 的等于运算符至少有三种方案:
- 重载全局的
==
运算符 - 重载 Foo 中的 hidden friend,即
friend bool operator==(Foo const &a, Foo const &b)
- 重载
bool operator==(Foo const &a) const
第一种方案会污染全局的名字空间,使得 Bar 和 Bar 之间也能通过转换两个参数进行比较;第三种方案允许第二个参数的隐式类型转换,但要求第一个参数必须是 Foo 类型;第二种方案只要求任一个参数为 Foo 类型,使得 ADL 能够参与找到这个函数,另一个参数则可以通过隐式类型转换来得到。
成员函数的劣势是由于查找方式和 this 指针的存在,函数失去了对称性。全局函数的劣势则是没有用上 ADL 的优势,hidden friends 在这种情况比较好。
Inheritance
发表于:
更新于:
public | protected | private | |
---|---|---|---|
共有继承 | public | protected | 不可见 |
私有继承 | private | private | 不可见 |
保护继承 | protected | protected | 不可见 |
可见继承属性就是对于来自基类的 public 和 protected 成员进行一个取最小权限的操作(定义权限 public > protected > private)。
struct 和 class 的数据定义没有区别。语法上区别是:struct 定义默认继承种类是 public,class 定义默认继承类型是 private,这和基类用 struct 还是 class 声明的无关。
继承多个类时,需要给不同的基类分别指定访问限定符。
子类继承父类的 protected 属性/方法之后,可以通过子类的指针去访问;子类也不能通过父类指针访问 protected 的成员。
Javascript 从 DOMNodeInserted 到 MutationObserver
发表于:
这段代码是在 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)
Lambda 在各 C++ 版本的演进
发表于:
更新于:
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 作为函数参数的类型。
Makefile
发表于:
$()
和 ${}
除了 $
之外,$()
和 ${}
都是 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
后缀。