极客时间已完结课程限时免费阅读

09 | 易用性改进 II:字面量、静态断言和成员函数说明符

09 | 易用性改进 II:字面量、静态断言和成员函数说明符-极客时间

09 | 易用性改进 II:字面量、静态断言和成员函数说明符

讲述:吴咏炜

时长13:34大小9.30M

你好,我是吴咏炜。
本讲我们继续易用性的话题,看看现代 C++ 带来的其他易用性改进。

自定义字面量

字面量(literal)是指在源代码中写出的固定常量,它们在 C++98 里只能是原生类型,如:
"hello",字符串字面量,类型是 const char[6]
1,整数字面量,类型是 int
0.0,浮点数字面量,类型是 double
3.14f,浮点数字面量,类型是 float
123456789ul,无符号长整数字面量,类型是 unsigned long
C++11 引入了自定义字面量,可以使用 operator"" 后缀 来将用户提供的字面量转换成实际的类型。C++14 则在标准库中加入了不少标准字面量。下面这个程序展示了它们的用法:
#include <chrono>
#include <complex>
#include <iostream>
#include <string>
#include <thread>
using namespace std;
int main()
{
cout << "i * i = " << 1i * 1i
<< endl;
cout << "Waiting for 500ms"
<< endl;
this_thread::sleep_for(500ms);
cout << "Hello world"s.substr(0, 5)
<< endl;
}
输出是:
i * i = (-1,0)
Waiting for 500ms
Hello
上面这个例子展示了 C++ 标准里提供的帮助生成虚数、时间和 basic_string 字面量的后缀。一个需要注意的地方是,我在上面使用了 using namespace std,这会同时引入 std 名空间和里面的内联名空间(inline namespace),包括了上面的字面量运算符所在的三个名空间:
std::literals::complex_literals
std::literals::chrono_literals
std::literals::string_literals
在产品项目中,一般不会(也不应该)全局使用 using namespace std(不过,为节约篇幅起见,专栏里的很多例子,特别是不完整的例子,还是默认使用了 using namespace std)。这种情况下,应当在使用到这些字面量的作用域里导入需要的名空间,以免发生冲突。在类似上面的例子里,就是在函数体的开头写:
using namespace std::literals::
chrono_literals;
等等。
要在自己的类里支持字面量也相当容易,唯一的限制是非标准的字面量后缀必须以下划线 _ 打头。比如,假如我们有下面的长度类:
struct length {
double value;
enum unit {
metre,
kilometre,
millimetre,
centimetre,
inch,
foot,
yard,
mile,
};
static constexpr double factors[] =
{1.0, 1000.0, 1e-3,
1e-2, 0.0254, 0.3048,
0.9144, 1609.344};
explicit length(double v,
unit u = metre)
{
value = v * factors[u];
}
};
length operator+(length lhs,
length rhs)
{
return length(lhs.value +
rhs.value);
}
// 可能有其他运算符
我们可以手写 length(1.0, length::metre) 这样的表达式,但估计大部分开发人员都不愿意这么做吧。反过来,如果我们让开发人员这么写,大家应该还是基本乐意的:
1.0_m + 10.0_cm
要允许上面这个表达式,我们只需要提供下面的运算符即可:
length operator"" _m(long double v)
{
return length(v, length::metre);
}
length operator"" _cm(long double v)
{
return length(v, length::centimetre);
}
如果美国国家航空航天局采用了类似的系统的话,火星气候探测者号的事故也许就不会发生了 [1]。当然,历史无法重来,而且 C++ 引入这样的语法已经是在事故发生之后十多年了……
关于自定义字面量的进一步技术细节,请参阅参考资料 [2]

二进制字面量

你一定知道 C++ 里有 0x 前缀,可以让开发人员直接写出像 0xFF 这样的十六进制字面量。另外一个目前使用得稍少的前缀就是 0 后面直接跟 0–7 的数字,表示八进制的字面量,在跟文件系统打交道的时候还会经常用到:有经验的 Unix 程序员可能会觉得 chmod(path, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH) 并不比 chmod(path, 0644) 更为直观。从 C++14 开始,我们对于二进制也有了直接的字面量:
unsigned mask = 0b111000000;
这在需要比特级操作等场合还是非常有用的。
不过,遗憾的是, I/O streams 里只有 dechexoct 三个操纵器(manipulator),而没有 bin,因而输出一个二进制数不能像十进制、十六进制、八进制那么直接。一个间接方式是使用 bitset,但调用者需要手工指定二进制位数:
#include <bitset>
cout << bitset<9>(mask) << endl;
111000000

数字分隔符

数字长了之后,看清位数就变得麻烦了。有了二进制字面量,这个问题变得分外明显。C++14 开始,允许在数字型字面量中任意添加 ' 来使其更可读。具体怎么添加,完全由程序员根据实际情况进行约定。某些常见的情况可能会是:
十进制数字使用三位的分隔,对应英文习惯的 thousand、million 等单位。
十进制数字使用四位的分隔,对应中文习惯的万、亿等单位。
十六进制数字使用两位或四位的分隔,对应字节或双字节。
二进制数字使用三位的分隔,对应文件系统的权限分组。
等等。
一些实际例子如下:
unsigned mask = 0b111'000'000;
long r_earth_equatorial = 6'378'137;
double pi = 3.14159'26535'89793;
const unsigned magic = 0x44'42'47'4E;

静态断言

C++98 的 assert 允许在运行时检查一个函数的前置条件是否成立。没有一种方法允许开发人员在编译的时候检查假设是否成立。比如,如果模板有个参数 alignment,表示对齐,那我们最好在编译时就检查 alignment 是不是二的整数次幂。之前人们用了一些模板技巧来达到这个目的,但输出的信息并不那么友善。比如,我之前使用的方法,会产生类似下面这样的输出:
能起作用,但不够直观。C++11 直接从语言层面提供了静态断言机制,不仅能输出更好的信息,而且适用性也更好,可以直接放在类的定义中,而不像之前用的特殊技巧只能放在函数体里。对于类似上面的情况,现在的输出是:
静态断言语法上非常简单,就是:
static_assert(编译期条件表达式,
可选输出信息);
产生上面的示例错误信息的代码是:
static_assert((alignment & (alignment - 1)) == 0,
"Alignment must be power of two");

default 和 delete 成员函数

在类的定义时,C++ 有一些规则决定是否生成默认的特殊成员函数。这些特殊成员函数可能包括:
默认构造函数
析构函数
拷贝构造函数
拷贝赋值函数
移动构造函数
移动赋值函数
生成这些特殊成员函数(或不生成)的规则比较复杂,感兴趣的话你可以查看参考资料 [3]。每个特殊成员函数有几种不同的状态:
隐式声明还是用户声明
默认提供还是用户提供
正常状态还是删除状态
这三个状态是可组合的,虽然不是所有的组合都有效。隐式声明的必然是默认提供的;默认提供的才可能被删除;用户提供的也必然是用户声明的。
如果成员和父类没有特殊原因导致对象不可拷贝或移动,在用户不声明这些成员函数的情况下,编译器会自动产生这些成员函数,即隐式声明、默认提供、正常状态。有特殊成员、用户声明的话,情况就非常复杂了:
没有初始化的非静态 const 数据成员和引用类型数据成员会导致默认提供的默认构造函数被删除。
非静态的 const 数据成员和引用类型数据成员会导致默认提供的拷贝构造函数、拷贝赋值函数、移动构造函数和移动赋值函数被删除。
用户如果没有自己提供一个拷贝构造函数(必须形如 Obj(Obj&)Obj(const Obj&);不是模板),编译器会隐式声明一个。
用户如果没有自己提供一个拷贝赋值函数(必须形如 Obj& operator=(Obj&)Obj& operator=(const Obj&);不是模板),编译器会隐式声明一个。
用户如果自己声明了一个移动构造函数或移动赋值函数,则默认提供的拷贝构造函数和拷贝赋值函数被删除。
用户如果没有自己声明拷贝构造函数、拷贝赋值函数、移动赋值函数和析构函数,编译器会隐式声明一个移动构造函数。
用户如果没有自己声明拷贝构造函数、拷贝赋值函数、移动构造函数和析构函数,编译器会隐式声明一个移动赋值函数。
……
我不鼓励你去死记硬背这些规则,而是希望你在项目和测试中体会其缘由。我认为这些规则还相当合理,虽然有略偏保守之嫌。尤其是关于移动构造和赋值:只要用户声明了另外的特殊成员函数中的任何一个,编译器就不默认提供了。不过嘛,缺省慢点总比缺省不安全要好……
我们这儿主要要说的是,我们可以改变缺省行为,在编译器能默认提供特殊成员函数时将其删除,或在编译器不默认提供特殊成员函数时明确声明其需要默认提供(不过,要注意,即使用户要求默认提供,编译器也可能根据其他规则将特殊成员函数标为删除)。
还是举例子来说明一下。对于下面这样的类,编译器看到有用户提供的构造函数,就会不默认提供默认构造函数:
template <typename T>
class my_array {
public:
my_array(size_t size);
private:
T* data_{nullptr};
size_t size_{0};
};
在没有默认初始化时,我们如果需要默认构造函数,就需要手工写一个,如:
my_array()
: data_(nullptr)
, size_(0) {}
可有了默认初始化之后,这个构造函数显然就不必要了,所以我们现在可以写:
my_array() = default;
再来一个反向的例子。我们[第 1 讲] 里的 shape_wrapper,它的复制行为是不安全的。我们可以像[第 2 讲] 里一样去改进它,但如果正常情况不需要复制行为、只是想防止其他开发人员误操作时,我们可以简单地在类的定义中加入:
class shape_wrapper {
shape_wrapper(
const shape_wrapper&) = delete;
shape_wrapper& operator=(
const shape_wrapper&) = delete;
};
在 C++11 之前,我们可能会用在 private 段里声明这些成员函数的方法,来达到相似的目的。但目前这个语法效果更好,可以产生更明确的错误信息。另外,你可以注意一下,用户声明成删除也是一种声明,因此编译器不会提供默认版本的移动构造和移动赋值函数。

override 和 final 说明符

overridefinal 是两个 C++11 引入的新说明符。它们不是关键词,仅在出现在函数声明尾部时起作用,不影响我们使用这两个词作变量名等其他用途。这两个说明符可以单个或组合使用,都是加在类成员函数声明的尾部。
override 显式声明了成员函数是一个虚函数且覆盖了基类中的该函数。如果有 override 声明的函数不是虚函数,或基类中不存在这个虚函数,编译器会报告错误。这个说明符的主要作用有两个:
给开发人员更明确的提示,这个函数覆写了基类的成员函数;
让编译器进行额外的检查,防止程序员由于拼写错误或代码改动没有让基类和派生类中的成员函数名称完全一致。
final 则声明了成员函数是一个虚函数,且该虚函数不可在派生类中被覆盖。如果有一点没有得到满足的话,编译器就会报错。
final 还有一个作用是标志某个类或结构不可被派生。同样,这时应将其放在被定义的类或结构名后面。
用法示意如下:
class A {
public:
virtual void foo();
virtual void bar();
void foobar();
};
class B : public A {
public:
void foo() override; // OK
void bar() override final; // OK
//void foobar() override;
// 非虚函数不能 override
};
class C final : public B {
public:
void foo() override; // OK
//void bar() override;
// final 函数不可 override
};
class D : public C {
// 错误:final 类不可派生
};

内容小结

今天我们介绍了现代 C++ 引入的另外几个易用性改进:自定义字面量,二进制字面量,数字分隔符,静态断言,default 和 delete 成员函数,及 override 和 final。同上一讲介绍的易用性改进一样,这些新功能可以改进代码的可读性,同时也不会带来额外的开销。在任何有条件使用满足新 C++ 标准的编译器的项目中,都应该考虑使用这些新特性。

课后思考

你最喜欢的 C++ 易用性改进是什么?为什么?
欢迎留言和我分享你的看法!

参考资料

[1] Wikipedia, “Mars Climate Orbiter”. https://en.wikipedia.org/wiki/Mars_Climate_Orbiter
[1a] 维基百科, “火星气候探测者号”. https://zh.wikipedia.org/zh-cn/ 火星氣候探測者號
[2] cppreference.com, “User-defined literals”. https://en.cppreference.com/w/cpp/language/user_literal
[2a] cppreference.com, “用户定义字面量”. https://zh.cppreference.com/w/cpp/language/user_literal
[3] cppreference.com, “Non-static member functions”, section “Special member functions”. https://en.cppreference.com/w/cpp/language/member_functions
[3a] cppreference.com, “非静态成员函数”, “特殊成员函数”部分. https://zh.cppreference.com/w/cpp/language/member_functions
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 4

提建议

上一篇
08 | 易用性改进 I:自动类型推断和初始化
下一篇
10 | 到底应不应该返回对象?
 写留言

精选留言(23)

  • 木瓜777
    2019-12-17
    您好,您有没有感觉比较好的开源c++项目推荐? 希望从别人的项目中学到一些经验,谢谢!

    作者回复: C++ 项目入门都不容易,要找自己有兴趣的领域是关键。除了第 6 讲评论里推荐的那些,可以考虑下面两个(我将来也会讲到): - EasyLogging++ - Catch2

    共 2 条评论
    11
  • zhengfan
    2020-07-01
    吴老师,您好。 请问您在static_assert中使用的例子,是不是应该判断alignment && (alignment & (alignment - 1) ==0)? 另外,这个算法真是精巧👍。效率之高让我甚至一度怀疑命题的等价性。证明了一下确实是等价的。

    作者回复: 0 也是 2 的整数次幂,没毛病。😁 这是很标准的算法,我很早就看到过。 从实用的角度,这一个判断可能确实不够。我的实际代码里有: STATIC_ASSERT(alignment::value > 0 && alignment::value <= 8192, Bad_alignment); STATIC_ASSERT((alignment::value & (alignment::value - 1)) == 0, Alignment_must_be_power_of_two);

    共 3 条评论
    3
  • 三味
    2019-12-20
    如果对一个函数声明了final, 我觉得没有必要在添加override了吧. override就是明确声明这是一个继承来的函数, final同样也是这个意思, 只不过final更霸道, 后续的不要在继承了! 如果从一开始就不想让别的函数去继承而写final, 那就根本没必要去virtual它. 何必要在虚函数表中添加一个没有继承作用的虚函数呢? PS: 数字分隔符和自定义字面量真是学到了. 在9102年的尾巴, 我才知道有这么邪道的用法... PPS: 前些日子看了一些国外游戏大牛各种喷C++的帖子. 本章的所有内容应该都是他们喷的范围吧... 那些人特别看中编译时间, 追求极致的运行效率... 有个人专门对比了int a=7; 和 int a{7}的性能差别...从编译到运行时间... 利用宏展开的方式, 对这两个例子分别做了百万次展开, 如果用vs测试都能爆IDE内存的级别... 我觉得对于中小型对性能不是特别敏感的程序, 这些还是很有用的. PPPS: 最近对Data-oriented design感兴趣, 不知道作者以后是否有开这类理论实战课程的计划捏? 我搜索上面喷神, 就是从这里开始搜索出来的...
    展开

    作者回复: 对,final override 合法但不必要。 当然不是所有内容都是被那个游戏开发人士喷的。他抱怨的是会导致编译速度下降,以及非优化编译性能差的那部份功能。通常都是模板相关的。所以这一讲的内容不属于其中。

    4
  • Geek3726
    2021-10-29
    硬着头皮看了基础篇,才发现,自己是没有基础的; 去补补C++的知识再来,虽然在公司开发C++的项目,但大部份的代码都是C语言写的,落后了。
    2
  • 小奶狗
    2020-09-15
    代码如下: #include <iostream> #include <memory> using namespace std; class dummy{ }; class Boy{ public: Boy(){ cout<<"boy constructor"<<endl; } Boy(Boy& m){ cout<<"boy copy constructor"<<endl; } Boy(Boy&& m){ cout<<"boy move constructor"<<endl; } }; class Man{ public: Man(){ cout<<"man constructor "<<endl; } Man(Boy boy){ cout<<"man constructor with boy "<<endl; } Man(Man &m){ cout<<"copy constructor"<<endl; } Man(Man&& m){ cout<<"move constructor"<<endl; } private: Boy a; }; int main(){ dummy d; dummy d2; d = d2; dummy d3 = d2; std::cout << "***************" <<std::endl; Boy&& boy = Boy(); Boy& b = boy; std::cout << "/////////////" <<std::endl; make_shared<Man>(boy); std::cout << "###############" <<std::endl; make_shared<Man>(move(boy)); } 输出的结果: *************** boy constructor ///////////// boy copy constructor boy constructor 疑惑:为什么会调用Boy的构造函数呢? man constructor with boy ############### boy move constructor boy constructor man constructor with boy Process finished with exit code 0 老师,如上“疑惑”所述,为什么会调用Boy的构造函数呢?我觉得不应该调用Boy的构造函数,因为调用了boy 拷贝构造,然后再调用Man的拷贝构造,就可以了。想不通。
    展开

    作者回复: 你在 Man 类里有私有成员变量 Boy a,又没有去初始化,那不就默认构造了么。 如果你想用参数 boy 去拷贝构造 a,你需要写: Man(Boy boy) : a(boy) { cout << "man constructor with boy " << endl; } 这样会拷贝构造两次。我想你真正需要的可能是(我加上了拷贝构造函数的 const): class Boy { public: Boy() { cout << "boy constructor" << endl; } Boy(const Boy& m) { cout << "boy copy constructor" << endl; } Boy(Boy&& m) { cout << "boy move constructor" << endl; } }; class Man { public: Man() { cout << "man constructor " << endl; } Man(const Boy& boy) : a(boy) { cout << "man constructor with boy " << endl; } Man(Man& m) { cout << "copy constructor" << endl; } Man(Man&& m) { cout << "move constructor" << endl; } private: Boy a; };

    2
  • 王旧业
    2020-03-15
    length的例子里,字面量operator""实现中参数是long double,但是length成员变量value使用的是double,这是有意为之吗?

    作者回复: 哦,这是因为 operator"" 的参数要求是 long double(不是 double)。而一般的类里,double 的精度就够用了。倒不算什么刻意为之吧。

    2
  • geek
    2021-03-05
    老师,关于子类中重新定义虚函数这个动作我之前认为是重写(overwrite),但也碰到有人说是重载(overload),另外我看文章中说的是覆写(就是override的翻译吧?)。 我个人倾向于overwrite或者override,因为重新定义虚函数这个动作涉及了多个类,而重载是指同一个类中的一组同名但形参个数或者形参类型不同的方法。我认为override是包含overwrite的,overwrite只说子类重新实现基类虚函数这种情况,而override则是覆盖,只要子类有了和基类中同名的方法,都会隐藏父类中的同名方法,如果子类能创建对象,必须要实现虚函数,因而就会隐藏基类中的同名方法。 老师看下我说的有问题吗?
    展开

    作者回复: 对。override 的函数的参数是一样的,子类覆盖父类。overload 的函数的参数是不一样的,根据参数来选择合适的函数。两者不能混淆。

    1
  • Milittle
    2020-02-25
    之前用default delete override final较多 static_assert 和 自定义字面量和二进制字面量用的少 争取以后可以使用在自己的项目中 提供便利 老师的硬货很多 受益匪浅 感谢

    作者回复: 小改进,适合的就用起来。但也不用强求为了用而用。

    1
  • 小一日一
    2019-12-16
    我最喜欢的C++易用性改进及理由: auto: 少打字 scope for:少打字 类成员默认补始化语法:少打字 default 和 delete 成员函数:简化对类行为的控制难度 自定义字面量:代码看起来舒服。
    展开

    作者回复: 都用上了吗?很好啊。

    共 2 条评论
    1
  • 瓜农
    2022-06-04
    一直有种感觉,相对于java,c++的语法设计略显随意不够自洽。 譬如const/final在java里面就final搞定了,vitual这个关键字也感觉比较鸡肋。 老师怎么看

    作者回复: 如果你不需要性能,那你可以有抽象和优雅。见 Python。 如果你不需要抽象,那你可以有简单和性能(但程序员用起来会很费力)。见 C。 如果你不需要向后兼容性,用几十年的经验设计一种全新的语言,那你也至少可以做到兼顾抽象、优雅和性能(简单是简单不了了)。见 Rust。 如果你啥都想要:性能,抽象,向后兼容性。那你就忘掉优雅吧,你会得到一个类似于 C++ 的东西。除非有人在 1980 之前能开天眼,那也许能设计出一个更优雅的东西。 但那也不会是 Rust——在 C++ 诞生的年代,Rust 的编译速度恐怕没有任何人能承担得起。 也不会是 Java。虚函数是有性能代价的,甚至是巨大的性能代价。Java 的虚拟机都是要用到 C++ 的。 另外,const 为什么要用 final 来修饰?我觉得 Java 的这个设计也很没道理。C++ 里 final 基本不需要,也是受了其他语言的影响才有的,并且是个说明符,不是关键字。我在项目里从来没写过 final(override 还是经常需要的)。

    1
  • 2022-02-11
    auto 和 for的语法糖

    作者回复: 严肃地讲,语法糖对于易用性非常重要。

  • 常振华
    2021-09-30
    唯一喜欢的易用性改造就是列表初始化,其它的反而增加了复杂度。

    作者回复: 这也只是你的一家之言。大部分人最喜欢的是 auto。

    1
  • 201200861
    2020-09-04
    老师,你好,如何看移动构造函数和拷贝函数,是编译器提供的还是用户自己写的?

    作者回复: 你自己提供了,编译器自然就不会提供了。头文件里瞟一眼啰。如果是别人提供的类定义,从使用者的角度,不去看类定义,应该是无从分辨的。 从使用者的角度,容易分辨的,只能是用户不提供这些特殊成员函数时,编译器有没有自动提供。使用一下,不报错,自然就是编译器自动提供了。

  • 赵国辉
    2020-07-22
    老师,有没有对c++虚函数表实现原理讲解比较透彻的文章或者书籍推荐一下。对这方面比较感兴趣。

    作者回复: 直接搜索就好。我看结果很匹配的。

  • tr
    2020-03-01
    第四版中文版中给出的代码是这样的: constexpr int ipow(int x, int n) { return (n > 0) ? x * ipow(n - 1) : 1; } template<char c> constexpr int b3_helper() { static_assert(c < '3', "not a ternary digit"); return c; } template<char c,char ... tail> constexpr int b3_helper() { static_assert(c < '3', "not a ternary digit"); return ipow(3, sizeof...(tail)) * (c - '0') + b3_helper(tail...); } template<char...chars> constexpr int operator"" _b3() { return b3_helper(chars...); }
    展开

    作者回复: 嗯,这明显是代码后来改过了。初版的代码应该没有好好测过,问题还不止一个。 顺着原先代码的思路也能实现的,但应该是下面这个样子(可参考第 15 讲): #include <stdexcept> constexpr int ipow(int x, int n) { return n > 0 ? x * ipow(x, n - 1) : 1; } template <typename C> constexpr int b3_helper(C c) { if (c < '0' || c >= '3') { throw std::invalid_argument("not a ternary digit"); } return c - '0'; } template <typename C, typename... Tail> constexpr int b3_helper(C c, Tail... tail) { if (c < '0' || c >= '3') { throw std::invalid_argument("not a ternary digit"); } return ipow(3, sizeof...(tail)) * (c - '0') + b3_helper(tail...); } template <char... chars> constexpr int operator"" _b3() { return b3_helper(chars...); } 第一个 b3_helper 还可以进一步精简成: constexpr int b3_helper() { return 0; }

    共 3 条评论
  • tr
    2020-02-29
    是19.2.6节三进制的那个例子,b3_helper的调用有问题,vs2019社区版报的是“应输入0个参数,却提供了3个”,但是把圆括号换成模板参数的尖括号又报告“未找到匹配的重载”。像这种怎么修改合适呢?

    作者回复: 我不知道你看到的是什么,我这边是编译通过了。完整代码如下: #include <iostream> constexpr int ipow(int x, int n) { return n > 0 ? x * ipow(x, n - 1) : 1; } template <char...> struct helper; template <char c> struct helper<c> { static_assert( '0' <= c && c < '3', "not a ternary digit"); static constexpr int value() { return c - '0'; } }; template <char c, char... tail> struct helper<c, tail...> { static_assert( '0' <= c && c < '3', "not a ternary digit"); static constexpr int value() { return (c - '0') * ipow(3, sizeof...(tail)) + helper<tail...>::value(); } }; template <char... chars> constexpr int operator"" _b3() { return helper<chars...>::value(); } int main() { std::cout << 201_b3 << std::endl; }

    共 3 条评论
  • tr
    2020-02-29
    老师,本贾尼在他的《c++程序设计语言》中480页写了一种叫模板字面值常量的语法,不过他的代码编译不过。如果您有时间的话能不能帮忙看下是不是真的有这种写法还是说其他什么原因

    作者回复: 我没有中文版,我也不确定你说的是哪个版本。 我看到第四版的19.2.6节似乎有类似你说的东西。请详细说一下你指的是哪一节的哪个例子,使用什么编译器,具体错误信息是什么。 单独起一个新的评论,不要回复这一个。

  • 皮皮侠
    2020-02-26
    用得最顺手的就是在for循环里auto,override在Qt里继承类重写虚函数时用到。其他的default和delete以前初学时写过几个,现在在项目里倒用得少了,以后尽量多用多试,毕竟不影响性能!谢谢老师解析了这么多C++新特性;)

    作者回复: 最方便的确实是在循环里用 auto,尤其在基于范围的循环里。其他的也可以慢慢用起来。

  • zKerry
    2020-02-24
    呃,这些东西在c#里都有

    作者回复: 首先,语言之间互相学习是正常的。 其次,C# 里有数字分隔符和二进制字面量是在 C# 7.0。换句话说,晚于 C++14。 另外,我不知道 C# 有静态断言和自定义字面量的支持。

  • 中年男子
    2019-12-24
    既然有了这些特性, 我觉的就得在平时开发中用起来,不用就没有用,完全浪费了大神的研究

    作者回复: 对的,一定要用起来!