为什么Herb Sutter';s CppCon 2014年讲座(回到基础:现代C+;+;风格)? CpPCon 2014的回溯到基本:现代C++风格,他把幻灯片28()指的是这种模式: class employee { std::string name_; public: void set_name(std::string name) noexcept { name_ = std::move(name); } };

为什么Herb Sutter';s CppCon 2014年讲座(回到基础:现代C+;+;风格)? CpPCon 2014的回溯到基本:现代C++风格,他把幻灯片28()指的是这种模式: class employee { std::string name_; public: void set_name(std::string name) noexcept { name_ = std::move(name); } };,c++,c++11,stl,C++,C++11,Stl,他说这是有问题的,因为当使用临时调用set_name()时,noexcept ness并不强(他使用短语“noexcept ish”) 现在,我一直在我自己最近的C++代码中使用了上面的模式,主要是因为它节省了我每次键入两个StimeNAME.()的副本,是的,我知道每次强制拷贝构建都会有点低效,但是我是个懒惰的人。然而,Herb的短语“This noexcept is problemble”让我担心,因为我在这里没有发现问题:std::string的移动赋值操作符是noexcept,它的析构函

他说这是有问题的,因为当使用临时调用set_name()时,noexcept ness并不强(他使用短语“noexcept ish”)


现在,我一直在我自己最近的C++代码中使用了上面的模式,主要是因为它节省了我每次键入两个StimeNAME.()的副本,是的,我知道每次强制拷贝构建都会有点低效,但是我是个懒惰的人。然而,Herb的短语“This noexcept is problemble”让我担心,因为我在这里没有发现问题:std::string的移动赋值操作符是noexcept,它的析构函数也是noexcept,所以在我看来,上面的set_name()似乎保证是noexcept。在set_name()准备参数时,我确实看到编译器在set_name()之前抛出了一个潜在的异常,但我很难认为这是有问题的


稍后在幻灯片32中,Herb明确指出,上述是一种反模式。有人能给我解释一下为什么我会因为懒惰而编写糟糕的代码吗?

有两种方法可以调用这些方法

  • 对于
    rvalue
    参数,只要参数类型的
    move构造函数
    是noexcept,就没有问题(在
    std::string
    的情况下,最有可能是noexcept),在任何情况下都最好使用条件noexcept(确保参数是noexcept)
  • 使用
    lvalue
    参数,在这种情况下,将调用参数类型的
    copy构造函数
    ,几乎可以肯定它将需要一些分配(这可能会抛出)

在这种情况下,可能会错过使用,最好避免。
的客户端假定没有按照指定抛出异常,但在有效、可编译、无可疑的
C++11
中可以抛出异常。

有两个原因可以解释为什么按值传递可能比按常量引用传递更好

  • 更有效率
  • 不例外
  • 在类型为
    std::string
    的成员的setter的情况下,他通过显示按常量引用传递通常会产生较少的分配(至少对于
    std::string
    ),驳斥了按值传递更有效的说法

    他还揭露了允许setter成为
    noexcept
    的说法,因为noexcept声明具有误导性,因为在复制参数的过程中仍然可能发生异常

    因此,他得出结论,至少在这种情况下,通过常量引用传递要优于通过值传递。然而,他确实提到了传递值对于构造函数来说是一种潜在的好方法


    我确实认为仅用std::string的例子不足以推广到所有类型,但它确实对通过值传递昂贵的复制参数而便宜的移动参数的做法提出了质疑,至少出于效率和异常原因。

    其他人已经讨论了上面的
    noexcept
    推理

    赫伯花了更多的时间讨论效率方面的问题。问题不在于分配,而在于不必要的取消分配。当您将一个
    std::string
    复制到另一个
    std::string中时,如果有足够的空间容纳要复制的数据,则复制例程将重用目标字符串的分配存储。在执行移动分配时,必须释放目标字符串的现有存储,因为它从源字符串接管存储。“复制并移动”习惯用法强制解除分配始终发生,即使您没有传递临时文件。这就是演讲后面所展示的糟糕表现的根源。他的建议是改为使用const ref,如果您确定需要它,则为r值引用提供一个重载。这将使您两全其美:为非临时存储复制到现有存储中,避免释放,并为临时存储移动,在临时存储中,您将以一种或另一种方式支付释放费用(移动之前的目标释放或复制之后的源释放)

    以上内容不适用于构造函数,因为成员变量中没有要释放的存储。这很好,因为构造函数通常接受多个参数,如果需要为每个参数执行const-ref/r-value-ref重载,那么最终会出现组合式的构造函数重载爆炸


    现在的问题是:有多少类在复制时重用存储,如std::string?我猜std::vector会,但除此之外我不确定。我知道我从来没有写过这样重用存储的类,但我写过很多包含字符串和向量的类。对于不重用存储的类,遵循Herb的建议不会对您造成伤害,您将首先使用sink函数的复制版本进行复制,如果您确定复制对性能的影响太大,那么您将进行r值引用重载以避免复制(就像您对std::string所做的那样)。另一方面,对于std::string和其他重用存储的类型,使用“复制和移动”确实会对性能产生明显的影响,而且这些类型在大多数代码中可能会有很多用途。我现在遵从Habor的建议,但在我认为问题完全解决之前,需要仔细考虑一下这一点(可能有一篇博客文章,我没有时间写这些)。在这种情况下,当您已经在中分配了存储时,按值获取可能是低效的,并且会导致不必要的分配。但是按
    const&
    执行几乎同样糟糕,就好像您获取原始的C字符串并将其传递给函数一样,会发生不必要的分配

    你应该带什么
    class employee {
      std::string name_;
    public:
      template<class T>
      void set_name(T&& name) noexcept { name_ = std::forward<T>(name); }
    };
    
    class employee {
      std::string name_;
    public:
      template<class T>
      std::enable_if_t<std::is_convertible<T,std::string>::value>
      set_name(T&& name) noexcept { name_ = std::forward<T>(name); }
    };
    
    template<class C>
    struct string_view {
      // could be private:
      C const* b=nullptr;
      C const* e=nullptr;
    
      // key component:
      C const* begin() const { return b; }
      C const* end() const { return e; }
    
      // extra bonus utility:
      C const& front() const { return *b; }
      C const& back() const { return *std::prev(e); }
    
      std::size_t size() const { return e-b; }
      bool empty() const { return b==e; }
    
      C const& operator[](std::size_t i){return b[i];}
    
      // these just work:
      string_view() = default;
      string_view(string_view const&)=default;
      string_view&operator=(string_view const&)=default;
    
      // myriad of constructors:
      string_view(C const* s, C const* f):b(s),e(f) {}
    
      // known continuous memory containers:
      template<std::size_t N>
      string_view(const C(&arr)[N]):string_view(arr, arr+N){}
      template<std::size_t N>
      string_view(std::array<C, N> const& arr):string_view(arr.data(), arr.data()+N){}
      template<std::size_t N>
      string_view(std::array<C const, N> const& arr):string_view(arr.data(), arr.data()+N){}
      template<class... Ts>
      string_view(std::basic_string<C, Ts...> const& str):string_view(str.data(), str.data()+str.size()){}
      template<class... Ts>
      string_view(std::vector<C, Ts...> const& vec):string_view(vec.data(), vec.data()+vec.size()){}
      string_view(C const* str):string_view(str, str+len(str)) {}
    private:
      // helper method:
      static std::size_t len(C const* str) {
        std::size_t r = 0;
        if (!str) return r;
        while (*str++) {
          ++r;
        }
        return r;
      }
    };
    
    class employee {
      std::string name_;
    public:
      void set_name(string_view<char> name) noexcept { name_.assign(name.begin(),name.end()); }
    };