C++11 语言扩展 – 类
=default
和 =delete
现在可以直接表达“禁止复制”的常见惯用法
class X {
// ...
X& operator=(const X&) = delete; // Disallow copying
X(const X&) = delete;
};
反之,我们也可以明确表示我们希望默认复制行为
class Y {
// ...
Y& operator=(const Y&) = default; // default copy semantics
Y(const Y&) = default;
};
手动明确写出默认行为充其量是多余的,并且有两个缺点:它有时会生成比编译器生成的默认行为效率更低的代码,并且会阻止类型被认为是 POD。然而,在 C++11 之前的代码中,关于复制操作的注释以及(更糟糕的是)用户明确定义旨在提供默认行为的复制操作并不少见。让编译器实现默认行为更简单,更不容易出错,并且通常会生成更好的目标代码。
=default
机制可用于任何具有默认值的函数。=delete
机制可用于任何函数。例如,我们可以像这样消除不需要的转换
struct Z {
// ...
Z(long long); // can initialize with a long long
Z(long) = delete; // but not anything smaller
};
另请参见
- [N2326==07-0186] Lawrence Crowl: 默认和删除的函数。
- [N3174=100164] B. Stroustrup: 要移动还是不移动。对与生成的复制和移动操作相关的问题的分析。已批准。
有关替代方案的更多历史背景,请参阅
- [N1717==04-0157] Francis Glassborow 和 Lois Goldthwaite: 显式类和默认定义(早期提案)。
- Bjarne Stroustrup: 类默认值的控制(死胡同)。
默认移动和复制的控制
默认情况下,一个类有五个操作
- 复制赋值
- 复制构造函数
- 移动赋值
- 移动构造函数
- 析构函数
如果您声明其中任何一个,则必须考虑所有并显式定义或 =default
您想要的那些。将复制、移动和析构视为密切相关的操作,而不是可以自由混合和匹配的独立操作——您可以指定任意组合,但只有少数组合在语义上有意义。
如果用户显式指定(声明、定义、=default
或 =delete
)了任何移动、复制或析构函数,则默认情况下不会生成移动。如果用户显式指定(声明、定义、=default
或 =delete
)了任何复制或析构函数,则默认情况下会生成任何未声明的复制操作,但此行为已被弃用,因此不要依赖它。例如
class X1 {
X1& operator=(const X1&) = delete; // Disallow copying
};
这隐式也禁止移动 X1
。允许复制初始化,但已弃用。
class X2 {
X2& operator=(const X2&) = default;
};
这隐式也禁止移动 X2
。允许复制初始化,但已弃用。
class X3 {
X3& operator=(X3&&) = delete; // Disallow moving
}
这隐式也禁止复制 X3
。
class X4 {
~X4() = delete; // Disallow destruction
}
这隐式也禁止移动 X4
。允许复制,但已弃用。
如果您声明了这五个函数中的一个,则应显式声明所有函数。例如
template<class T>
class Handle {
T* p;
public:
Handle(T* pp) : p{pp} {}
~Handle() { delete p; } // user-defined destructor: no implicit copy or move
Handle(Handle&& h) :p{h.p} { h.p=nullptr; } // transfer ownership
Handle& operator=(Handle&& h) { delete p; p=h.p; h.p=nullptr; return *this; } // transfer ownership
Handle(const Handle&) = delete; // no copy
Handle& operator=(const Handle&) = delete;
// ...
};
另请参见
- [N2326==07-0186] Lawrence Crowl: 默认和删除的函数。
- [N3174=100164] B. Stroustrup: 要移动还是不移动。对与生成的复制和移动操作相关的问题的分析。已批准。
委托构造函数
在 C++98 中,如果您希望两个构造函数执行相同的操作,请重复自己或调用“一个 init()
函数”。例如
class X {
int a;
void validate(int x) { if (0<x && x<=max) a=x; else throw bad_X(x); }
public:
X(int x) { validate(x); }
X() { validate(42); }
X(string s) { int x = lexical_cast<int>(s); validate(x); }
// ...
};
冗长会阻碍可读性,重复容易出错。两者都妨碍了可维护性。因此,在 C++11 中,我们可以根据另一个构造函数来定义一个构造函数
class X {
int a;
public:
X(int x) { if (0<x && x<=max) a=x; else throw bad_X(x); }
X() :X{42} { }
X(string s) :X{lexical_cast<int>(s)} { }
// ...
};
另请参见
- C++ 草案第 12.6.2 节
- N1986==06-0056 Herb Sutter 和 Francis Glassborow: 委托构造函数(修订版 3)。
- ECMA-372,用于描述此功能在被提议用于 ISO C++ 之前在 C++/CLI 中最初设计时的详细信息。
类内成员初始化器
在 C++98 中,只有整型类型的 static const
成员可以在类内初始化,并且初始化器必须是常量表达式。这些限制确保编译器可以在编译时进行初始化。例如
int var = 7;
class X {
static const int m1 = 7; // ok
const int m2 = 7; // error: not static
static int m3 = 7; // error: not const
static const int m4 = var; // error: initializer not constant expression
static const string m5 = "odd"; // error: not integral type
// ...
};
C++11 的基本思想是允许在声明非静态数据成员的地方(在其类中)对其进行初始化。然后,构造函数可以在需要运行时初始化时使用初始化器。考虑
class A {
public:
int a = 7;
};
这等价于
class A {
public:
int a;
A() : a(7) {}
};
这节省了一些打字,但真正的优势在于具有多个构造函数的类。通常,所有构造函数都使用成员的公共初始化器
class A {
public:
A(): a(7), b(5), hash_algorithm("MD5"), s("Constructor run") {}
A(int a_val) : a(a_val), b(5), hash_algorithm("MD5"), s("Constructor run") {}
A(D d) : a(7), b(g(d)), hash_algorithm("MD5"), s("Constructor run") {}
int a, b;
private:
HashingFunction hash_algorithm; // Cryptographic hash to be applied to all A instances
std::string s; // String indicating state in object lifecycle
};
hash_algorithm
和 s
各自只有一个默认值的事实在代码的混乱中丢失了,并且在维护期间很容易成为问题。相反,我们可以将数据成员的初始化分解出来
class A {
public:
A(): a(7), b(5) {}
A(int a_val) : a(a_val), b(5) {}
A(D d) : a(7), b(g(d)) {}
int a, b;
private:
HashingFunction hash_algorithm{"MD5"}; // Cryptographic hash to be applied to all A instances
std::string s{"Constructor run"}; // String indicating state in object lifecycle
};
如果成员既由类内初始化器初始化,又由构造函数初始化,则只执行构造函数的初始化(它“覆盖”默认值)。所以我们可以进一步简化
class A {
public:
A() {}
A(int a_val) : a(a_val) {}
A(D d) : b(g(d)) {}
int a = 7;
int b = 5;
private:
HashingFunction hash_algorithm{"MD5"}; // Cryptographic hash to be applied to all A instances
std::string s{"Constructor run"}; // String indicating state in object lifecycle
};
另请参见
- C++ 草案章节“随处可见的一两个词”;请参阅提案。
- [N2628=08-0138] Michael Spertus 和 Bill Seymour: 非静态数据成员初始化器。
继承构造函数
人们有时会对普通作用域规则适用于类成员这一事实感到困惑。特别是,基类的成员与派生类的成员不在同一作用域中
struct B {
void f(double);
};
struct D : B {
void f(int);
};
B b; b.f(4.5); // fine
D d; d.f(4.5); // surprise: calls f(int) with argument 4
在 C++98 中,我们可以将一组重载函数从基类“提升”到派生类中
struct B {
void f(double);
};
struct D : B {
using B::f; // bring all f()s from B into scope
void f(int); // add a new f()
};
B b; b.f(4.5); // fine
D d; d.f(4.5); // fine: calls D::f(double) which is B::f(double)
Stroustrup 说过:“除了历史上的偶然之外,没有什么能阻止将此用于构造函数以及普通成员函数。” C++11 提供了这种功能
class Derived : public Base {
public:
using Base::f; // lift Base's f into Derived's scope -- works in C++98
void f(char); // provide a new f
void f(int); // prefer this f to Base::f(int)
using Base::Base; // lift Base constructors Derived's scope -- new in C++11
Derived(char); // provide a new constructor
Derived(int); // prefer this constructor to Base::Base(int)
// ...
};
如果您选择,您仍然可以通过在需要初始化的新成员变量的派生类中继承构造函数来搬起石头砸自己的脚
struct B1 {
B1(int) { }
};
struct D1 : B1 {
using B1::B1; // implicitly declares D1(int)
int x;
};
void test()
{
D1 d(6); // Oops: d.x is not initialized
D1 e; // error: D1 has no default constructor
}
您可以通过使用成员初始化器来避免这种情况
struct D1 : B1 {
using B1::B1; // implicitly declares D1(int)
int x{0}; // note: x is initialized
};
void test()
{
D1 d(6); // d.x is zero
}
另请参见
- C++ 草案第 12.9 节。
- [N1890=05-0150] Bjarne Stroustrup 和 Gabriel Dos Reis: 初始化和初始化器(初始化相关问题的概述以及建议的解决方案)。
- [N1898=05-0158] Michel Michaud 和 Michael Wong: 转发和继承构造函数。
- [N2512=08-0022] Alisdair Meredith, Michael Wong, Jens Maurer: 继承构造函数(修订版 4)。
重写控制:override
派生类中的函数重写基类中的函数不需要特殊的关键字或注解。例如
struct B {
virtual void f();
virtual void g() const;
virtual void h(char);
void k(); // not virtual
};
struct D : B {
void f(); // overrides B::f()
void g(); // doesn't override B::g() (wrong type)
virtual void h(char); // overrides B::h()
void k(); // doesn't override B::k() (B::k() is not virtual)
};
这可能会导致混淆(程序员是什么意思?),如果编译器不警告可疑代码,则会产生问题。例如,
- 程序员是否打算重写
B::g()
?(几乎肯定是)。 - 程序员是否打算重写
B::h(char)
?(可能不是,因为存在多余的显式virtual
)。 - 程序员是否打算重写
B::k()
?(可能,但那是不可能的)。
为了让程序员更明确地进行重写,我们现在有了“上下文关键字” override
struct D : B {
void f() override; // OK: overrides B::f()
void g() override; // error: wrong type
virtual void h(char); // overrides B::h(); likely warning
void k() override; // error: B::k() is not virtual
};
标记为 override
的声明仅在存在要重写的函数时才有效。h()
的问题不一定会被捕获(因为它根据语言定义不是错误),但它很容易被诊断。
override
只是一个上下文关键字,所以你仍然可以把它作为标识符使用
int override = 7; // not recommended
另请参见
- 标准:10 派生类 [class.derived] [9]
- 标准:10.3 虚函数 [class.virtual]
- [N3234==11-0004] Ville Voutilainen: 从类头中移除 explicit。
- [N3151==10-0141] Ville Voutilainen: 重写控制关键字。更早、更详细的设计。
- [N3163==10-0153] Herb Sutter: 使用上下文关键字进行重写控制。另一种更早、更详细的设计。
- ECMA-372,用于描述此功能在被提议用于 ISO C++ 之前在 C++/CLI 中设计的更详细版本。
- [N2852==09-0042] V. Voutilainen, A. Meredith, J. Maurer, and C. Uzdavinis: 显式虚重写。基于属性的早期设计。
- [N1827==05-0087] C. Uzdavinis 和 A. Meredith: C++ 的显式重写语法。原始提案。
重写控制:final
有时,程序员希望阻止虚函数被重写。这可以通过添加修饰符 final
来实现。例如
struct B {
virtual void f() const final; // do not override
virtual void g();
};
struct D : B {
void f() const; // error: D::f attempts to override final B::f
void g(); // OK
};
阻止重写有合理的理由,但应该指出,许多用于激励 final
的示例都基于对虚函数成本的错误假设(通常基于其他语言的经验)。因此,如果您强烈希望添加 final
修饰符,请仔细检查原因是否合乎逻辑:如果有人定义了一个重写该虚函数的类,是否可能出现语义错误?添加 final
关闭了未来该类的用户可能会为某个您未曾考虑的类提供更好的函数实现的可能性。如果您不想保留该选项,那么您最初为什么将该函数定义为 virtual
?迄今为止遇到的最合理的答案通常是:这是一个框架中的基本函数,框架构建者需要重写它,但对于普通用户来说重写它是不安全的。对此类说法要保持警惕,并确保 final
确实合适。
如果您想要的是性能(内联)或者您根本不希望重写,那么通常最好一开始就不要将函数定义为 virtual
。这不是 Java。
final
只是一个上下文关键字,因此你仍然可以把它作为标识符使用
int final = 7; // not recommended
另请参见
- 标准:10 派生类 [class.derived] [9]
- 标准:10.3 虚函数 [class.virtual]
显式转换运算符
C++98 提供了隐式和 explicit
构造函数;也就是说,由声明为 explicit
的构造函数定义的转换只能用于显式转换,而其他构造函数也可以用于隐式转换。例如
struct S { S(int); }; // "ordinary constructor" defines implicit conversion
S s1(1); // ok
S s2 = 1; // ok
void f(S);
f(1); // ok (but that's often a bad surprise -- what if S was vector?)
struct E { explicit E(int); }; // explicit constructor
E e1(1); // ok
E e2 = 1; // error (but that's often a surprise)
void f(E);
f(1); // error (protects against surprises -- e.g. std::vector's constructor from int is explicit)
然而,构造函数不是定义转换的唯一机制。如果我们不能修改一个类,我们可以定义一个从不同类转换的运算符。例如
struct S { S(int) { } /* ... */ };
struct SS {
int m;
SS(int x) :m(x) { }
operator S() { return S(m); } // because S don't have S(SS); non-intrusive
};
SS ss(1);
S s1 = ss; // ok; like an implicit constructor
S s2(ss); // ok ; like an implicit constructor
void f(S);
f(ss); // ok; like an implicit constructor
不幸的是,C++98 没有 explicit
转换运算符,这主要是因为有问题的例子要少得多。C++11 通过允许转换运算符为 explicit
来弥补这一疏忽。例如
struct S { S(int) { } };
struct SS {
int m;
SS(int x) :m(x) { }
explicit operator S() { return S(m); } // because S don't have S(SS)
};
SS ss(1);
S s1 = ss; // error; like an explicit constructor
S s2(ss); // ok ; like an explicit constructor
void f(S);
f(ss); // error; like an explicit constructor
另请参见
- 标准:12.3 转换
- [N2333=07-0193] Lois Goldthwaite, Michael Wong, and Jens Maurer: 显式转换运算符(修订版 1)。