C ++ 17中的湿滑地方

图片

近年来,C ++取得了长足的进步,要跟上语言的所有微妙和复杂性是非常非常困难的。 新标准不是遥不可及的,但是,引入新趋势并不是最快,最简单的过程,因此,尽管在C ++ 20之前还有一段时间,但我建议刷新或发现当前标准的一些特别“易滑”的地方语言。

今天,我将告诉您为什么constexpr不能代替宏,结构化绑定的“内部”是什么,以及它的“陷阱”,复制省略号现在总是可以使用并且您可以毫不犹豫地写出任何回报是真的。

如果您不怕弄脏自己的手,钻研舌头的“内部”,欢迎来到Cat。



如果constexpr


让我们从最简单的一个开始- if constexpr允许您丢弃条件表达式分支,即使在编译阶段,条件表达式分支也无法满足期望的条件。

看来这是#if宏的替代品,以关闭“额外”逻辑? 不行 一点也不。

首先,这样的if具有宏不可用的属性-在内部,您可以计算可以constexprbool任何constexpr表达式。 嗯,其次,废弃分支的内容在语法和语义上都应该正确。

由于第二个要求,例如, if constexpr无法使用if constexpr ,则从构造语言的角度来看,不存在的函数(无法以这种方式显式分离依赖于平台的代码)或不好(例如,“ void T = 0; ”)。

使用if constexpr什么? 要点是在模板中。 它们有一个特殊的规则:实例化模板时,不实例化丢弃的分支。 这使得编写某种程度上取决于模板类型的属性的代码变得更加容易。

但是,在模板中,请不要忘记分支中的代码至少对于某些实例实例(甚至是纯潜在实例)必须正确,因此,根本static_assert(false)在其中一个分支中编写例如static_assert(false) (有必static_assert(false) static_assert取决于某些模板相关的参数)。

范例:

 void foo() {    //    ,       if constexpr ( os == OS::win ) {        win_api_call(); //         }    else {        some_other_os_call(); //  win      } } 

 template<class T> void foo() {    //    ,    T      if constexpr ( os == OS::win ) {        T::win_api_call(); //  T   ,    win    }    else {        T::some_other_os_call(); //  T   ,         } } 

 template<class T> void foo() {    if constexpr (condition1) {        // ...    }    else if constexpr (condition2) {        // ...    }    else {        // static_assert(false); //          static_assert(trait<T>::value); // ,   ,  trait<T>::value   false    } } 

要记住的事情


  1. 所有分支中的代码必须正确。
  2. 在模板内部,未实例化丢弃的分支的内容。
  3. 对于模板实例化的至少一个纯粹潜在的变体,任何分支内的代码必须正确。

结构化绑定




在C ++ 17中,出现了一种相当方便的机制来分解各种类似tuple的对象,使您可以方便,简洁地将其内部元素绑定到命名变量:

 //     —    : for (const auto& [key, value] : map) {    std::cout << key << ": " << value << std::endl; } 

类似于元组的对象,我指的是这样的对象,对于该对象,已知其在编译时可用的内部元素的数量(从“ tuple”-具有固定数量的元素(向量)的有序列表)。

这样的定义属于以下定义: std::pairstd::tuplestd::array ,形式为“ T a[N] ”的数组以及各种自写结构和类。

停止...您可以在结构绑定中使用自己的结构吗? 破坏者:可以(尽管有时您必须努力工作(但以下内容有更多内容))。

如何运作


结构链接的工作值得单独写一篇文章,但是由于我们专门谈论的是“湿滑”的地方,因此我将尝试简要地解释一切的工作原理。

该标准提供以下用于定义绑定的语法:

attr (可选) cv-auto ref-operator (可选)[ identifier-list ] 表达式

  • attr可选属性列表;
  • cv-auto自动使用可能的const / volatile修饰符;
  • ref-operator可选参考说明符(&或&&);
  • identifier-list -新变量名称的列表;
  • expression是一个表达式,该表达式会导致一个类似于元组的对象用于绑定(表达式可以采用“ = expr ”,“ {expr} ”或“ (expr) ”的形式)。

重要的是要注意, identifier-list中名称的数量必须与expression产生的对象中元素的数量匹配。

这一切都允许您编写以下形式的结构:

 const volatile auto && [a,b,c] = Foo{}; 

在这里,我们到达第一个“湿滑”的地方:满足“ auto a = expr; “,您通常是指类型“ a ”将由表达式“ expr ”计算,并且您希望在表达式中“ const auto& [a,b,c] = expr; “将完成相同的操作,只有“ a,b,c ”的类型将是“ expr ”的相应const&元素类型。

事实是不同的: cv-auto ref-operator说明cv-auto ref-operator用于计算不可见变量的类型,将expr计算的结果分配给该变量(即,编译器将“ const auto& [a,b,c] = expr ”替换为“ const auto& e = expr “)。

因此,出现了一个新的不可见实体(以下将其称为{e}),但是该实体非常有用:例如,它可以实现临时对象(因此,您可以安全地将它们连接起来:“ const auto& [a,b,c] = Foo {}; “)。

紧随其后的是编译器进行的第二次替换:如果为{e}推导的类型不是引用,则expr的结果将被复制到{e}。

identifier-list变量将具有哪些类型? 首先,这些将不完全是变量。 是的,它们的行为类似于真实的普通变量,但是区别仅在于它们内部引用与之关联的实体,而此类“引用”变量中的decltype将产生该变量所引用的实体的类型:

 std::tuple<int, float> t(1, 2.f); auto& [a, b] = t; // decltype(a) — int, decltype(b) — float ++a; // ,  « »,   t std::cout << std::get<0>(t); //  2 

类型本身定义如下:

  1. 如果{e}是一个数组( T a[N] ),则类型将为-T,cv-修饰符将与该数组的修饰符重合。
  2. 如果{e}为E类型并支持元组接口,则定义以下结构:

     std::tuple_size<E> 

     std::tuple_element<i, E> 

    和功能:

     get<i>({e}); //  {e}.get<i>() 

    那么每个变量的类型将是std::tuple_element_t<i, E>
  3. 在其他情况下,变量的类型将对应于要执行绑定的结构元素的类型。

因此,如果非常简短,将通过结构链接执行以下步骤:

  1. 基于类型exprcv-ref修饰符的类型计算和不可见实体{e}的初始化。
  2. 创建伪变量并将其绑定到{e}元素。

在结构上链接您的类/结构


链接它们的结构的主要障碍是C ++中缺乏反射。 即使是看起来似乎肯定也必须确定内部结构如何安排的编译器,也很难过:访问修饰符(公共/私有/受保护)和继承使事情变得非常复杂。

由于此类困难,对它们的类​​的使用的限制非常严格(至少目前为止P1061P1096 ):

  1. 一个类的所有内部非静态字段都必须来自同一基类,并且在使用时它们必须可用。
  2. 否则该类必须实现“反射”(支持元组接口)。

 //  «»  struct A { int a; }; struct B : A {}; struct C : A { int c; }; class D { int d; }; auto [a] = A{}; //  (a -> A::a) auto [a] = B{}; //  (a -> B::A::a) auto [a, c] = C{}; // : a  c    auto [d] = D{}; // : d — private void D::foo() {    auto [d] = *this; //  (d   ) } 

元组接口的实现允许您使用任何类进行绑定,但是它看起来有点麻烦并且带来另一个陷阱。 让我们立即使用一个示例:

 //  ,      int   class Foo; template<> struct std::tuple_size<Foo> : std::integral_constant<std::size_t, 1> {}; template<> struct std::tuple_element<0, Foo> { using type = int&; }; class Foo { public: template<std::size_t i> std::tuple_element_t<i, Foo> const& get() const; template<std::size_t i> std::tuple_element_t<i, Foo> & get(); private: int _foo = 0; int& _bar = _foo; }; template<> std::tuple_element_t<0, Foo> const& Foo::get<0>() const { return _bar; } template<> std::tuple_element_t<0, Foo> & Foo::get<0>() { return _bar; } 

现在我们绑定:

 Foo foo; const auto& [f1] = foo; const auto [f2] = foo; auto& [f3] = foo; auto [f4] = foo; 

现在是时候考虑一​​下我们有什么类型了吗? (任何可以立即回答的人都应该得到美味的甜心。)

 decltype(f1); decltype(f2); decltype(f3); decltype(f4); 

正确答案
 decltype(f1); // int& decltype(f2); // int& decltype(f3); // int& decltype(f4); // int& ++f1; //     foo._foo,  {e}    const 


为什么会这样呢? 答案在于std::tuple_element的默认专业化:

 template<std::size_t i, class T> struct std::tuple_element<i, const T> { using type = std::add_const_t<std::tuple_element_t<i, T>>; }; 

std::add_const不会将const添加到引用类型,因此Foo的类型将始终为int&

如何赢得这个? 只需为const Foo添加专门化:

 template<> struct std::tuple_element<0, const Foo> { using type = const int&; }; 

然后所有类型都将被期望:

 decltype(f1); // const int& decltype(f2); // const int& decltype(f3); // int& decltype(f4); // int& ++f1; //     

顺便说一句,对于例如std::tuple<T&> ,也是如此
-即使对象本身将是常量,也可以获得对内部元素的非常量引用。

要记住的事情


  1. cv-auto ref [a1..an] = expr ”中的“ cv-auto ref ”是指不可见变量{e}。
  2. 如果未引用推断的类型{e},则将通过复制来初始化{e}(小心地使用“重量级”类)。
  3. 绑定变量是“隐式”链接(它们的行为类似于链接,尽管decltype为它们返回非引用类型(除非变量引用链接))。
  4. 使用引用类型进行绑定时必须小心。

返回值优化(rvo,复制省略)




也许这是C ++ 17标准中讨论得最多的功能之一(至少在我的朋友圈中如此)。 确实是这样:C ++ 11带来了移动的语义,这大大简化了对象“内部”的传递和各种工厂的创建,而C ++ 17似乎使得通常不必考虑如何从某种工厂方法返回对象,-现在所有内容都不应复制,总的来说,“很快所有内容都会在火星上绽放” ...

但是,让我们有点现实:优化返回值并不是最容易实现的事情。 我强烈建议您观看cppcon2018的演示文稿:Arthur O'Dwyer“ 返回值优化:比它看起来更难 ”,作者在其中解释了为什么这样做很困难。

短扰流板:

有一个“返回值的插槽”之类的东西。 此插槽本质上只是堆栈,由调用并传递给被调用方的位置分配。 如果被调用的代码确切知道将返回哪个单个对象,则可以直接在该插槽中直接创建它(前提是该对象的大小和类型与插槽相同)。

随之而来的是什么? 让我们通过示例将其分开。

此处一切都会很好-NRVO将起作用,该对象将立即在“插槽”中构造:

 Base foo1() { Base a; return a; } 

在这里不再可能明确确定哪个对象应该是结果,因此将隐式调用move构造函数 (c ++ 11):

 Base foo2(bool c) { Base a,b; if (c) { return a; } return b; } 

这里有点复杂...由于返回值的类型与声明的类型不同,因此您不能隐式调用move ,因此默认情况下调用了复制构造函数。 为了防止这种情况发生,您需要显式调用move

 Base foo3(bool c) { Derived a,b; if (c) { return std::move(a); } return std::move(b); } 

看起来这和foo2 ,但是三元运算符是一件非常奇怪的事情 ……

 Base foo4(bool c) { Base a, b; return std::move(c ? a : b); } 

foo4类似,但也有不同的类型,因此完全需要move

 Base foo5(bool c) { Derived a, b; return std::move(c ? a : b); } 

从示例中可以看到,即使在看似微不足道的情况下,人们仍然必须思考如何返回意义……是否有任何方法可以简化您的生活? 有:一段时间以来,clang支持诊断是否需要显式调用move ,新标准中有一些建议( P1155P0527 )可以减少显式调用的必要性。

要记住的事情


  1. RVO / NRVO仅在以下情况下有效:
    • 明确知道应该在“返回值槽”中创建哪个对象;
    • 返回对象和函数类型相同。
  2. 如果返回值不明确,则:
    • 如果返回的对象和函数的类型匹配,则将隐式调用move;
    • 否则,您必须显式调用move。
  3. 使用三元运算符时要小心:它很简洁,但是可能需要进行明确的操作。
  4. 最好使用带有有用诊断程序的编译器(或至少使用静态分析器)。

结论


但是我爱C ++;)

Source: https://habr.com/ru/post/zh-CN465181/


All Articles