从C++17开始,直接列表init与复制列表init与从prvalue复制init的差异示例



对于这3种初始化形式(对于某些类型的A(

A x = {<...>};
A x{<...>};
auto x = A{<...>};

自C++17以来,除了w.r.t.显式ctors(显式ctor会打破第一个(的差异之外,还有什么行为差异的例子?

(由于C++17保证的拷贝省略消除了拷贝;复制构造函数隐式转换也不应该在这里适用,因为auto…(

我通常知道C++初始化形式是如何工作的,很抱歉这个问题太宽泛/模糊,我只是对上面三种形式之间是否有任何(显著(差异感兴趣,或者它们可能基本上可以互换使用(除了提到的差异(。

我认为这里的差异很重要,尤其是因为隐式/显式的差异,我认为你太快就忽略了。

我要回答一个与你问的略有不同的问题:为什么要使用哪种形式?与之相反:有什么区别?

A x = {<...>};

如果可以编译,则首选此表单。

如果rhs表达式具有可显式转换为A的类型,则它不会编译。这是一件非常好的事情!这是该语言提供的一个安全功能。

类型作者通常(或者至少应该(为非常安全的转换保留隐式构造/转换。这种转换应该是无损的,并且不会改变rhs和lhs之间表达的基本语义。例如:milliseconds x = 3s;rhs是seconds,lhs是milliseconds。并且构造在两个持续时间之间转换,并且根本不会丢失任何信息。这是一个使用这种建筑形式的好地方。

通过使用这种形式,程序员说:只需给我A类型的构造函数的"安全集"。如果我在rhs上的表达式类型中存在错误,可能会导致不安全的转换,我希望在编译时了解相关信息。

A是积分型时,即使包括{}也有一个优点:

unsigned x = {i};

这遵循了"只给我安全的转换"的风格。但它为你的吊带增加了一条额外的腰带:只要给我一个不会缩小的转换。{}基本上是后C++中的一个额外的权宜之计,以弥补几十年前C中的设计错误。


但是有时你需要一个更大的锤子。有时显式转换是您想要和需要的。现在是的时候了

A x{<...>};

例如:milliseconds x{3};这将int转换为millisecond。尽管转换是无损的,但lhs的类型与rhs的类型不同。rhs可以代表3个任何东西。3个苹果。3 IRS通知。3年。这不是一个安全的隐式转换。std::lib知道这一点。如果您尝试,它将无法编译。尽管如此,有时这正是你需要做的。为这种情况保留此表格。

如果不遵循此建议,在某些情况下可能会导致运行时错误。其中两个在闪电般的演讲中得到了展示。


最后,这是一个很好的形式:

auto x = A{<...>}; 

当您希望x的类型为A时。当A不是一个容易拼写的类型,甚至没有出现在rhs上时,它尤其好。

我最喜欢使用这种形式的例子是在std::chrono::round实现中:

template <class To, class Rep, class Period>
constexpr
To
round(const duration<Rep, Period>& d)
{
auto t0 = floor<To>(d);
auto t1 = t0 + To{1};
if (t1 == To{0} && t0 < To{0})
t1 = -t1;
auto diff0 = d - t0;  // here
auto diff1 = t1 - d;  // and here
if (diff0 == diff1)
{
if (t0 - duration_cast<To>(t0/2)*2 == To{0})
return t0;
return t1;
}
if (diff0 < diff1)
return t0;
return t1;
}

标记为"here and here"的行(通常(导致diff0diff1的类型非常复杂:有几种不同的拼写方式。它就是common_type_v<duration<Rep, Period>, To>。对于代码的读者来说,这种类型到底是什么并不重要。唯一需要知道的是,这种类型将代表两个操作数的确切差异。


总之,这不是"最佳形式"。它们都是你工具箱里的好工具。诀窍是知道什么时候该用哪个。如果你擅长它,你将比绝大多数C++程序员更熟练。

最新更新