44

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

在第 5 项中,斯科特说使用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 = bool{features(w)[5]};

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

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

4

4 回答 4

26

遵循 C++ 标准:

§ 8.5 初始化器[dcl.init]

  1. 表单中发生的初始化

    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 转换intbool是)。

方案 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};,它们是:

  1. auto变量必须始终被初始化。也就是说,你不能写auto a;,就像你可以写容易出错一样int a;

  2. 现代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>;
    
  3. 使用复制省略和非显式复制/移动构造函数,与语法相比,它的成本为零。T x{y}

  4. 当类型之间存在细微差别时,它会更加明确:

     unique_ptr<Base> p = make_unique<Derived>(); // subtle difference
    
     auto p = unique_ptr<Base>{make_unique<Derived>()}; // explicit and clear
    
  5. {}保证没有隐式转换和缩小。

但他也提到了auto x = T{}一般形式的一些缺点,这已经在这篇文章中描述过:

  1. 即使编译器可以省略右侧的临时变量,它也需要一个可访问的、非删除的和非显式的复制构造函数:

     auto x = std::atomic<int>{}; // fails to compile, copy constructor deleted
    
  2. 如果没有启用省略(例如-fno-elide-constructors),那么移动不可移动的类型会导致昂贵的副本:

     auto a = std::array<int,50>{};
    
于 2014-09-01T16:18:48.720 回答
15

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

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

  • 尽可能使用最弱的演员/转换。

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

这里static_cast是不必要的强。隐式转换就可以了。所以避免演员。

于 2014-09-01T14:43:22.897 回答
8

书中背景:

尽管在std::vector<bool>概念上持有bools,但operator[]forstd::vector<bool>不会返回对容器元素的引用(这是std::vector::operator[]除了 之外的所有类型的返回值bool)。相反,它返回一个类型的对象std::vector<bool>::reference(一个嵌套在里面的类std::vector<bool>)。

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

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

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

于 2014-09-01T15:15:38.660 回答
3

谁能告诉我,为什么使用这个成语是有利的?

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

bool priority = features(w)[5];

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

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

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

这段代码不是为了优化最短的灵活代码而编写的,而是为了结果的明确性(显然是一致性——因为我认为它不是唯一用 auto 声明的变量)。

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

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

于 2014-09-01T14:54:11.360 回答