为什么我更喜欢明确给出类型的"显式类型化初始化器"习惯用法

Mar*_*rio 43 c++ effective-c++ c++11

我最近从Scott Meyers那里购买了新的Effective现代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
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好.

但斯科特对此的解决方案是所谓的"明确类型化的初始化器成语".这个想法是,在初始化器上使用static_cast,如下所示:

auto priority = static_cast<bool>(features(w)[5]);
Run Code Online (Sandbox Code Playgroud)

但这不仅会导致更多的输入,而且还会明确说明应该推断出的类型.你基本上失去了auto超过明确给定类型的优点.

任何人都可以告诉我,为什么使用这个成语是有利的?


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

auto priority = static_cast<bool>(features(w)[5]);
Run Code Online (Sandbox Code Playgroud)

代替:

bool priority = features(w)[5];
Run Code Online (Sandbox Code Playgroud)

@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]};
Run Code Online (Sandbox Code Playgroud)

第一名仍然是我的最爱.它的打字较少,而且与其他三个一样明确.

有关保证初始化的观点并不真正成立,因为我无论如何都要声明变量,而不是以某种方式初始化它们.关于缩小的另一个论点在快速测试中没有得到很好的解决(参见http://ideone.com/GXvIIr).

Pio*_*cki 23

遵循C++标准:

§8.5初始化程序 [dcl.init]

  1. 在表单中发生的初始化

    T x = a;
    
    Run Code Online (Sandbox Code Playgroud)

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

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

auto x = features(w)[5];
Run Code Online (Sandbox Code Playgroud)

作为代表任何形式的自动/模板类型的复制初始化(一般推导类型)的那个,就像:

template <typename A>
void foo(A x) {}

foo(features(w)[5]);
Run Code Online (Sandbox Code Playgroud)

以及:

auto bar()
{
    return features(w)[5];
}
Run Code Online (Sandbox Code Playgroud)

以及:

auto lambda = [] (auto x) {};
lambda(features(w)[5]);
Run Code Online (Sandbox Code Playgroud)

所以关键是,我们不能总是只是"将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]));
Run Code Online (Sandbox Code Playgroud)

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

foo(bool{features(w)[5]});
Run Code Online (Sandbox Code Playgroud)

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

每当您想强制变量的类型时,请使用auto x = static_cast<T>(y);而不是T x{y};.

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

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

作为涉及类型推导的场景的解决方案,建议如下:

如果编译器的常规类型推导机制不是您想要的,请使用static_cast<T>(y).


UPDATE

并回答您更新的问题,以下哪个首选项应该更适合:

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]};
Run Code Online (Sandbox Code Playgroud)

场景1

首先,想象std::vector<bool>::reference不能隐式转换为bool:

struct BoolReference
{
    explicit operator bool() { /*...*/ }
};
Run Code Online (Sandbox Code Playgroud)

现在,bool priority = features(w)[5];无法编译,因为它不是一个明确的布尔上下文.其他人将工作正常(只要operator bool()可访问).

情景2

其次,我们假设它std::vector<bool>::reference旧方式实现,虽然转换运算符不是explicit,但它返回int:

struct BoolReference
{
    operator int() { /*...*/ }
};
Run Code Online (Sandbox Code Playgroud)

在签名的变化关断auto priority = bool{features(w)[5]};初始化,如使用{}防止变窄(其转换的intbool是).

场景3

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

struct MyBool
{
    explicit MyBool(bool b) {}
};
Run Code Online (Sandbox Code Playgroud)

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

个人态度

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

auto priority = bool{features(w)[5]};
Run Code Online (Sandbox Code Playgroud)

因为它引入一个明确的布尔上下文(其是在壳体细我们希望分配该值以布尔变量),并防止变窄(在其它类型的,未容易兑换到布尔的情况下),这样,当一个错误/警告被触发,我们可以诊断features(w)[5] 实际是什么.


更新2

我最近看过Herb Sutter在2014年CppCon上发表的题为" 回归基础 " 的演讲!现代C++风格的精华,在那里他介绍了关于为什么要一个喜欢的一些要点明确的类型初始化auto x = T{y};形式(虽然它是不一样的使用auto x = static_cast<T>(y),因此并不是所有的论点也适用)以上T x{y};,它们分别是:

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

  2. 现代C++风格更倾向于右侧的类型,就像在:

    a)文字:

    auto f = 3.14f;
    //           ^ float
    
    Run Code Online (Sandbox Code Playgroud)

    b)用户定义的文字:

    auto s = "foo"s;
    //            ^ std::string
    
    Run Code Online (Sandbox Code Playgroud)

    c)功能声明:

    auto func(double) -> int;
    
    Run Code Online (Sandbox Code Playgroud)

    d)命名为lambdas:

    auto func = [=] (double) {};
    
    Run Code Online (Sandbox Code Playgroud)

    e)别名:

    using dict = set<string>;
    
    Run Code Online (Sandbox Code Playgroud)

    f)模板别名:

    template <class T>
    using myvec = vector<T, myalloc>;
    
    Run Code Online (Sandbox Code Playgroud)

    因此,再添加一个:

    auto x = T{y};
    
    Run Code Online (Sandbox Code Playgroud)

    与我们在左侧有名称的样式一致,并在右侧键入初始化程序,可简要描述为:

    <category> name = <type> <initializer>;
    
    Run Code Online (Sandbox Code Playgroud)
  3. 使用copy-elision和非显式复制/移动构造函数,与语法相比,它具有零成本T x{y}.

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

     unique_ptr<Base> p = make_unique<Derived>(); // subtle difference
    
     auto p = unique_ptr<Base>{make_unique<Derived>()}; // explicit and clear
    
    Run Code Online (Sandbox Code Playgroud)
  5. {} 保证没有隐含的转换,也没有缩小范围.

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

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

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

     auto a = std::array<int,50>{};
    
    Run Code Online (Sandbox Code Playgroud)


Ben*_*igt 15

我没有在我面前的这本书,所以我不知道是否有更多的背景.

但是要回答你的问题,不,在这个特定的例子中使用auto+ static_cast不是一个好的解决方案.它违反了另一条准则(我从未见过任何例外情况):

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

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

这里static_cast不必要地强大.隐式转换会很好.所以避免演员.


Ser*_*lov 8

书中的背景:

虽然std::vector<bool>概念上持有boolS,operator[]对于std::vector<bool>一个参考不返回到所述容器的元件(这是std::vector::operator[]为每一个类型的不同之处返回bool).相反,它返回一个类型的对象std::vector<bool>::reference(嵌套在里面的类std::vector<bool>).

当您使用auto与外部库时,没有任何优势,更多的是防止错误.

我想,这是这种成语的主要思想.你应该明确并强制自动行为正确.

顺便说一下,这是关于GotW关于汽车的好文章.

  • 好吧,`std :: vector <T> :: operator []`_always_返回一个`std :: vector <T> :: reference`; 它恰好发生在`std :: vector <bool> :: reference`不是`bool&`:)(替换为`const_reference`和`bool const&`for`std :: vector <T> :: operator [] const `). (2认同)