为啥我应该更喜欢“显式类型的初始化程序”习语而不是显式给出类型

Posted

技术标签:

【中文标题】为啥我应该更喜欢“显式类型的初始化程序”习语而不是显式给出类型【英文标题】:Why should I prefer the "explicitly typed initializer" idiom over explicitly giving the type为什么我应该更喜欢“显式类型的初始化程序”习语而不是显式给出类型 【发布时间】:2014-09-01 13:45:21 【问题描述】:

我最近从 Scott Meyers 那里购买了新的 Effective Modern C++ 并正在阅读它。但是我遇到了一件让我很烦恼的事情。

在第 5 项中,Scott 说使用 auto 是一件很棒的事情。它节省了输入,在大多数情况下为您提供正确的类型,并且它可能不受类型不匹配的影响。我完全理解这一点,并认为auto 也是一件好事。

但是在第 6 项中,斯科特说每个硬币都有两个面。同样,auto 可能会推断出完全错误的类型,例如用于代理对象。

你可能已经知道这个例子:

class Widget;
std::vector<bool> features(Widget w);

Widget w;

bool priority = features(w)[5]; // this is fine

auto priority = features(w)[5]; // this result in priority being a proxy
                                // to a temporary object, which will result
                                // in undefined behavior on usage after that
                                // line

到目前为止,一切都很好。

但 Scott 对此的解决方案是所谓的“显式类型初始化习语”。这个想法是,像这样在初始化器上使用 static_cast:

auto priority = static_cast<bool>(features(w)[5]);

但这不仅会导致更多的输入,还意味着您还显式地声明了应该推导的类型。与显式给定类型相比,您基本上失去了auto 的两个优点。

谁能告诉我,为什么使用这个成语有好处?


首先澄清一下,我的问题是我为什么要写:

auto priority = static_cast<bool>(features(w)[5]);

代替:

bool priority = features(w)[5];

@Sergey 在GotW 上提供了一篇关于此主题的好文章的链接,该文章部分回答了我的问题。

指南:考虑声明局部变量 auto x = type expr ;当您确实想明确提交类型时。显示代码显式请求转换是自文档化的,它保证变量将被初始化,并且不会允许意外的隐式缩小转换。仅当您确实想要显式缩小时,才使用 ( ) 而不是 。

这基本上让我想到了一个相关的问题。我应该选择这四种中的哪一种?

bool priority = features(w)[5];

auto priority = static_cast<bool>(features(w)[5]);

auto priority = bool(features(w)[5]);

auto priority = boolfeatures(w)[5];

第一名仍然是我的最爱。它不像其他三个那样打字和明确。

关于保证初始化的观点并不真正成立,因为无论如何我都在声明变量,而不是在我能够以某种方式初始化它们之前。而关于缩小范围的另一个论点在a quick test 中没有得到很好的解决。

【问题讨论】:

我读过他的书(不是这本),我怀疑他没有解释。 @Niall:据我了解,问题不是为什么需要这样做,而是为什么将类型放在static_cast 而不是auto @PiotrS。为什么将类型放在static_cast 中在评论中进行了解释(即避免UB)。我认为作者想说的是仅将他的解决方案用于特定情况,例如图示。当使用auto 不会产生任何影响时,就不需要使用它。 @40two: 不...问题是为什么 Scott 发明了一个看起来像 auto x = static_cast&lt;Y&gt;(z); 的成语可以表达为 Y x = z; 而且,Y x=e; 更好,因为auto x=static_cast&lt;Y&gt;(e); 激活explicit 转换,应谨慎使用。我怀疑注释说返回值不应该保留在调用的行之后可能是为了 C++。 【参考方案1】:

遵循 C++ 标准:

§ 8.5 初始化器[dcl.init]

    表单中发生的初始化

    T x = a;
    

    以及在参数传递、函数返回、抛出异常 (15.1)、处理异常 (15.3) 和聚合成员初始化 (8.5.1) 中称为复制初始化

我能想到书中给出的例子:

auto x = features(w)[5];

作为代表任何形式的copy-initialization自动/模板类型(一般推导类型),就像:

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&lt;T&gt; 移动到赋值的左侧”

相反,在上述任何示例中,我们需要显式指定所需的类型,而不是让编译器自行推断,如果后者可能导致未定义的行为

我的例子分别是:

/*1*/ foo(static_cast<bool>(features(w)[5]));

/*2*/ return static_cast<bool>(features(w)[5]);

/*3*/ lambda(static_cast<bool>(features(w)[5]));

因此,使用static_cast&lt;T&gt; 是一种强制所需类型的优雅方式,或者可以通过显式构造函数调用来表达:

foo(boolfeatures(w)[5]);

总而言之,我不认为这本书说:

当您想强制变量的类型时,请使用auto x = static_cast&lt;T&gt;(y); 而不是T xy;

对我来说,这听起来更像是一个警告:

auto 的类型推断很酷,但如果使用不当,可能会导致未定义的行为。

对于涉及类型推演的场景,提出以下解决方案:

如果编译器的常规类型推断机制不是您想要的,请使用static_cast&lt;T&gt;(y)


更新

并回答您更新的问题,应该更喜欢以下哪些初始化

bool priority = features(w)[5];

auto priority = static_cast<bool>(features(w)[5]);

auto priority = bool(features(w)[5]);

auto priority = boolfeatures(w)[5];

场景 1

首先,假设std::vector&lt;bool&gt;::reference不是隐式可转换为bool

struct BoolReference

    explicit operator bool()  /*...*/ 
;

现在,bool priority = features(w)[5];无法编译,因为它不是显式的布尔上下文。其他的都可以正常工作(只要operator bool() 可以访问)。

场景 2

其次,假设std::vector&lt;bool&gt;::reference旧式 实现,虽然转换运算符 不是explicit,但它返回int

struct BoolReference

    operator int()  /*...*/ 
;

签名的更改关闭 auto priority = boolfeatures(w)[5]; 初始化,因为使用 可以防止缩小(将int 转换为bool 是) .

场景 3

第三,如果我们谈论的根本不是bool,而是一些用户定义的类型,令我们惊讶的是,它声明了explicit构造函数:

struct MyBool

    explicit MyBool(bool b) 
;

令人惊讶的是,MyBool priority = features(w)[5]; 初始化将再次无法编译,因为复制初始化语法需要非显式构造函数。其他人会工作。

个人态度

如果我要从列出的四个候选中选择一个初始化,我会选择:

auto priority = boolfeatures(w)[5];

因为它引入了一个明确的布尔上下文(如果我们想将此值分配给布尔变量,这很好)并防止缩小(在其他类型的情况下,不容易转换为布尔),所以当触发了错误/警告,我们可以诊断features(w)[5] 究竟是什么


更新 2

我最近在 CppCon 2014 上观看了 Herb Sutter 题为 Back to the Basics! Essentials of Modern C++ Style 的演讲,其中他提出了一些观点,说明为什么人们应该更喜欢 auto x = Ty;显式类型初始化器形式(尽管它与auto x = static_cast&lt;T&gt;(y) 不同,因此并非所有参数都适用)T xy;,它们是:

    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 = Ty;
    

    与我们左侧有name,右侧有initializer的样式一致,可以简单描述为:

    <category> name = <type> <initializer>;
    

    T xy 语法相比,使用复制省略和非显式复制/移动构造函数零成本

    当类型之间存在细微差别时会更明确:

     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>;
    

【讨论】:

感谢您的输入,在这些情况下,我看到了使用 static_cast 的一些好处,但我不明白,为什么它与我原来的问题有关?我的意思是,仅仅因为某处还有一个auto 并不意味着你必须遵守同样的规则。 @Mario:因为应用了相同的推断类型的规则。 @Mario:我不认为这本书说:“当你想强制变量的类型时,使用auto x = static_cast&lt;T&gt;(y); 而不是T xy;”。在我看来,这只是本书给出了如何强制所需类型的提示,跳过了常规的类型推断机制。 我仍然不相信,这对于给定的代码是一个好主意,但我明白你对其他情况的看法。这可能就是 Scott 心中的想法。 比较auto x = y as T这四种情况并避免T xy;似乎毫无意义...【参考方案2】:

我面前没有这本书,所以我不知道是否还有更多上下文。

但要回答您的问题,不,在这个特定示例中使用 auto+static_cast 并不是一个好的解决方案。它违反了另一条准则(我从未见过有任何例外是合理的):

尽可能使用最弱的强制转换/转换。

不必要的强强制转换会破坏类型系统并阻止编译器生成诊断消息,以防程序中的其他地方发生更改以不兼容的方式影响转换。 (远距离行动,维护编程的恶魔)

这里的static_cast 太强了。隐式转换就可以了。所以避免演员表。

【讨论】:

我真的很喜欢你的回答,但我有一种感觉,@Piotr S. 的回答在书中的含义方面更具有方向性。 @Mario: 显式构造函数调用T varexpression 确实更好,因为它不会像static_cast 那样强烈地破坏类型系统。并且,在可能的情况下,T var = expression; 可能是首选,因为它甚至更弱,尽管不必要地允许显式构造函数调用可以说比引入强制转换的问题要少得多。【参考方案3】:

书中的上下文:

虽然std::vector&lt;bool&gt; 在概念上持有bools,但operator[] for std::vector&lt;bool&gt; 不会返回对容器元素的引用(这是std::vector::operator[] 为除bool 之外的所有类型返回的内容)。相反,它返回std::vector&lt;bool&gt;::reference 类型的对象(嵌套在std::vector&lt;bool&gt; 中的类)。

当您将 auto 与外部库一起使用时,没有任何优势,它更能防止错误。

我想,这就是这种成语的主要思想。您应该明确并强制 auto 正确运行。

顺便说一句,这里是 GotW 关于汽车的好文章。

【讨论】:

好吧,std::vector&lt;T&gt;::operator[] 总是 返回一个std::vector&lt;T&gt;::reference;碰巧std::vector&lt;bool&gt;::reference 不是bool&amp; :)(用const_referencebool const&amp; 替换std::vector&lt;T&gt;::operator[] const)。【参考方案4】:

谁能告诉我,为什么使用这个成语有好处?

我能想到的原因:因为它是明确的。考虑一下您将如何(本能地)阅读此代码(即,不知道 features 做了什么):

bool priority = features(w)[5];

“功能返回一些通用“布尔”值的可索引序列;我们将第五个读入priority”。

auto priority = static_cast<bool>(features(w)[5]);

“功能返回可显式转换为bool 的可索引值序列;我们将第五个值读入priority”。

编写此代码不是为了优化最短的灵活代码,而是为了结果的明确性(并且显然是一致性 - 因为我假设它不是唯一用 auto 声明的变量)。

priority 的声明中使用 auto 是为了保持代码对右侧任何表达式的灵活性。

也就是说,我更喜欢没有明确演员表的版本。

【讨论】:

除了(与第二个语句的并行性)第一个是“与布尔值兼容的值”,不一定已经是布尔值。或者这就是您使用“布尔”与bool 的目的? "在优先级声明中使用 auto 是为了保持代码灵活适应右侧的任何表达式" -- 但auto 可以生成除@987654330 之外的任何类型@ 当由表达式 static_cast&lt;bool&gt;(...) 初始化时?我想说“当然不是”,在这种情况下,选择auto 没有任何优势,但是 C++ 有一些奇怪的角落。 @j_random_hacker,事实是,我并不完全相信自己(即我可能会将变量声明为 bool 并完成它)。保留static_cast 的一个优点(我只是想到了这一点):重构时,看到显式转换可能表明对features 的调用应该被提取到返回bool 的函数中(特别是如果构造重复在客户端代码中)。 @BenVoigt,是的,这就是我的意思(我已经编辑了回复以使其更清晰)。不过,我对客户端代码的解释非常主观。

以上是关于为啥我应该更喜欢“显式类型的初始化程序”习语而不是显式给出类型的主要内容,如果未能解决你的问题,请参考以下文章

为啥我应该更喜欢 unsafe_unretained 限定符而不是为弱引用属性赋值? [复制]

为啥我应该使用 Amazon Kinesis 而不是 SNS-SQS?

为啥 NumPy 和 SciPy 有很多相同的功能?我应该更喜欢哪个? [复制]

为啥 Haskell 中的递归习语是“'n+1' and 'n'”而不是“'n' and 'n-1'”?

为啥 JSLint 更喜欢点符号而不是方括号?

为啥 `while ((ch = getchar()) != EOF)` 是常见的习语? [关闭]