c++ - 为什么我更喜欢 "explicitly typed initializer"成语而不是明确给出类型

标签 c++ c++11 effective-c++

我最近从 Scott Meyers 那里购买了新的 Effective现代 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 上提供了一篇好文章的链接关于这个话题,这部分回答了我的问题。

Guideline: Consider declaring local variables auto x = type{ expr }; when you do want to explicitly commit to a type. It is self-documenting to show that the code is explicitly requesting a conversion, it guarantees the variable will be initialized, and it won’t allow an accidental implicit narrowing conversion. Only when you do want explicit narrowing, use ( ) instead of { }.



这基本上让我想到了一个相关的问题。我应该选择这四种选择中的哪一种?
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]};

第一名仍然是我的最爱。它的输入更少,与其他三个一样明确。

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

最佳答案

遵循 C++ 标准:

§ 8.5 Initializers [dcl.init]

  1. The initialization that occurs in the form

    T x = a;
    

    as well as in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and aggregate member initialization (8.5.1) is called copy-initialization.



我可以想到书中给出的例子:
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]));

因此,使用 static_cast<T>是一种强制所需类型的优雅方式,或者可以通过显式构造函数调用来表示:
foo(bool{features(w)[5]});

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

Whenever you want to force the type of a variable, use auto x = static_cast<T>(y); instead of T x{y};.



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

The type inference with auto is cool, but may end up with undefined behavior if used unwisely.



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

If the compiler's regular type-deduction mechanism is not what you want, use 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]};

场景一

首先,想象一下std::vector<bool>::reference不是隐含的 可转换为 bool :
struct BoolReference
{
    explicit operator bool() { /*...*/ }
};

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

场景2

其次,让我们假设 std::vector<bool>::reference以旧方式实现,尽管转换运算符不是 explicit ,它返回 int反而:
struct BoolReference
{
    operator int() { /*...*/ }
};

签名变更关闭 auto priority = bool{features(w)[5]};初始化,如使用 {}防止缩小(将 int 转换为 bool 是)。

场景3

第三,如果我们不是在谈论 bool 呢?完全没有,但关于一些用户定义的类型,令我们惊讶的是,声明 explicit构造函数:
struct MyBool
{
    explicit MyBool(bool b) {}
};

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

个人态度

如果我要从列出的四个候选中选择一个初始化,我会选择:
auto priority = bool{features(w)[5]};

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

更新 2

我最近在 CppCon 2014 上观看了 Herb Sutter 题为 Back to the Basics! Essentials of Modern C++ Style 的演讲。 ,在那里他提出了一些关于为什么应该更喜欢 auto x = T{y}; 的显式类型初始化器的观点。形式(尽管它与 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,右边有type的样式是一致的,可以简单描述为:
    <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>{};
    
  • 关于c++ - 为什么我更喜欢 "explicitly typed initializer"成语而不是明确给出类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25607216/

    相关文章:

    c++ - "expected a ' > '"在类模板特化中?

    c++ - 使用类型特征为字符串迭代器专门化模板函数

    c++ - 阅读 Effective、More Effective 和 Effective Modern C++(和 STL)的首选顺序是什么?

    c++ - 使用unique_ptr并返回引用,或者我应该使用shared_ptr并根据需要进行复制

    c++ - 为什么叫运算符重载呢?

    c++ - 链接 pthread 禁用无锁 shared_ptr 实现

    c++ - 如何在Cygwin GCC 4.7.2下使用C++ 11的std::thread

    c++ - 有效 C++ : Item 41 - confusion about Implicit interfaces

    c++ - 前向声明包括,在声明之上包括(ClassFwd.h + Class.h)

    c++ - 在头文件中声明 Qt 类