遵循 C++ 标准:
§ 8.5 初始化器[dcl.init]
表单中发生的初始化
T x = a;
以及在参数传递、函数返回、抛出异常 (15.1)、处理异常 (15.3) 和聚合成员初始化 (8.5.1) 中称为复制初始化。
我可以想到书中给出的例子:
auto x = features(w)[5];
作为代表具有自动/模板类型(通常是推导类型)的任何形式的复制初始化的一种,就像:
template <typename A>
void foo(A x) {}
foo(features(w)[5]);
也:
auto bar()
{
return features(w)[5];
}
也:
auto lambda = [] (auto x) {};
lambda(features(w)[5]);
所以重点是,我们不能总是“将类型 T 从static_cast<T>
赋值的左侧移动”。
相反,在上述任何示例中,如果后者可能导致未定义的行为,我们需要显式指定所需的类型,而不是允许编译器自行推断:
分别是我的例子:
/*1*/ foo(static_cast<bool>(features(w)[5]));
/*2*/ return static_cast<bool>(features(w)[5]);
/*3*/ lambda(static_cast<bool>(features(w)[5]));
因此, usingstatic_cast<T>
是一种强制所需类型的优雅方式,或者可以通过显式构造函数调用来表达:
foo(bool{features(w)[5]});
总而言之,我不认为这本书说:
每当您想强制变量的类型时,请使用auto x = static_cast<T>(y);
而不是T x{y};
.
对我来说,这听起来更像是一个警告:
类型推断auto
很酷,但如果使用不当,可能会导致未定义的行为。
并且针对涉及类型推导的场景,提出以下解决方案:
如果编译器的常规类型推断机制不是您想要的,请使用static_cast<T>(y)
.
更新
并回答您更新的问题,应该更喜欢以下哪种初始化:
bool priority = features(w)[5];
auto priority = static_cast<bool>(features(w)[5]);
auto priority = bool(features(w)[5]);
auto priority = bool{features(w)[5]};
方案 1
首先,假设不能隐式转换std::vector<bool>::reference
为:bool
struct BoolReference
{
explicit operator bool() { /*...*/ }
};
现在,bool priority = features(w)[5];
不会编译,因为它不是显式的布尔上下文。其他的都可以正常工作(只要operator bool()
可以访问)。
方案 2
其次,假设std::vector<bool>::reference
以旧方式实现,虽然转换运算符is not explicit
,但它返回int
:
struct BoolReference
{
operator int() { /*...*/ }
};
签名中的更改会关闭auto priority = bool{features(w)[5]};
初始化,因为 using{}
可以防止缩小(将 an 转换int
为bool
是)。
方案 3
第三,如果我们根本不是在谈论bool
,而是在谈论一些用户定义的类型,令我们惊讶的是,它声明了explicit
构造函数:
struct MyBool
{
explicit MyBool(bool b) {}
};
MyBool priority = features(w)[5];
令人惊讶的是,初始化将再次无法编译,因为复制初始化语法需要非显式构造函数。其他人会工作。
个人态度
如果我要从列出的四个候选中选择一个初始化,我会选择:
auto priority = bool{features(w)[5]};
因为它引入了一个明确的布尔上下文(如果我们想将此值分配给布尔变量,这很好)并防止缩小(在其他类型的情况下,不容易转换为布尔),所以当一个错误/警告被触发,我们可以诊断到底features(w)[5]
是什么。
更新 2
我最近在CppCon 2014上观看了 Herb Sutter 的演讲,题为Back to the Basics!Essentials of Modern C++ Style,他提出了一些观点,说明为什么人们应该更喜欢 form 的显式类型初始化器auto x = T{y};
(尽管它与 with 不同auto x = static_cast<T>(y)
,因此并非所有参数都适用)T x{y};
,它们是:
auto
变量必须始终被初始化。也就是说,你不能写auto a;
,就像你可以写容易出错一样int a;
现代C++风格更喜欢右边的类型,就像在:
a) 字面量:
auto f = 3.14f;
// ^ float
b) 用户定义的文字:
auto s = "foo"s;
// ^ std::string
c) 函数声明:
auto func(double) -> int;
d) 命名的 lambda:
auto func = [=] (double) {};
e) 别名:
using dict = set<string>;
f) 模板别名:
template <class T>
using myvec = vector<T, myalloc>;
因此,再添加一个:
auto x = T{y};
与我们左侧有name,右侧有initializer的样式一致,可以简单描述为:
<category> name = <type> <initializer>;
使用复制省略和非显式复制/移动构造函数,与语法相比,它的成本为零。T x{y}
当类型之间存在细微差别时,它会更加明确:
unique_ptr<Base> p = make_unique<Derived>(); // subtle difference
auto p = unique_ptr<Base>{make_unique<Derived>()}; // explicit and clear
{}
保证没有隐式转换和缩小。
但他也提到了auto x = T{}
一般形式的一些缺点,这已经在这篇文章中描述过:
即使编译器可以省略右侧的临时变量,它也需要一个可访问的、非删除的和非显式的复制构造函数:
auto x = std::atomic<int>{}; // fails to compile, copy constructor deleted
如果没有启用省略(例如-fno-elide-constructors
),那么移动不可移动的类型会导致昂贵的副本:
auto a = std::array<int,50>{};