【问题标题】:Best way to create a setter function in C++在 C++ 中创建 setter 函数的最佳方法
【发布时间】:2020-10-23 11:18:45
【问题描述】:

我想编写一个模板函数,通过移动或复制接收参数。 我使用的最有效的方法是:

void setA(A a)
{
   m_a = std::move(a);
}

这里,当我们使用is时

A a;
setA(a);            // <<---- one copy ctor & one move ctor
setA(std::move(a)); // <<---- two move ctors

我最近发现用这种方式定义它,有两个功能:

void setA(A&& a)
{
   m_a = std::move(a);
}
void setA(const A& a)
{
   m_a = a;  // of course we can so "m_a = std::move(a);" too, since it will do nothing
}

会节省很多!

A a;
setA(a);            // <<---- one copy ctor
setA(std::move(a)); // <<---- one move ctor

这太棒了!对于一个参数...创建具有 10 个参数的函数的最佳方法是什么?!

void setAAndBAndCAndDAndEAndF...()

有人有什么想法吗? 谢谢!

【问题讨论】:

  • 也可以考虑void setA(A a) { std::swap(m_a, a); }
  • 最有效的方法是在构造函数中使用完美转发。
  • @WhozCraig:为什么?用m_a 的先前内容填充a 有什么好处,而不是让它“空”,当函数退出时它就会被销毁?
  • 如果你有一个简单的 setter 函数来设置任何值,你也许可以有一个公共数据成员。然后你可以只写obj.a = std::move(a);obj.a = a; 只需1 个副本或移动。此外,如果您使 setter 内联或对整个程序进行优化,并且您的移动构造函数没有副作用(它不应该),则可以优化第二步。
  • “最有效的”是没有一个函数首先需要 10 个参数。它在其他地方也会受到伤害,而不仅仅是参数的副本。只是说...

标签: c++ performance c++11 c++17


【解决方案1】:

setA(A&amp;&amp; a)setA(const A&amp; a) 两个 setter 版本可以使用 forwarding reference 组合成一个版本(也称为完美转发):

template<typename A>
void setA(A&& a)
{
   m_a = std::forward<A>(a);
}

然后编译器将根据值类别根据需要合成右值或左值引用版本。

这也解决了多值设置器的问题,因为会根据每个参数的值类别来合成正确的设置器。


话虽如此,请记住,setter 只是常规函数;在可以调用任何 setter 时,该对象在技术上已经构建。在setA 的情况下,如果A 有一个非平凡的构造函数,那么一个实例m_a 已经(默认)构造了,setA 实际上必须覆盖它。

这就是为什么在现代 C++ 中,重点往往不是 move- vs. copy-,而是 in-place construction vs. move/copy。

例如:

struct A {
    A(int x) : m_x(x) {}

    int m_x;
};

struct B {
    template<typename T>
    B(T&& a) : m_a(std::forward<T>(a)) {}

    A m_a;
};

int main() {
    B b{ 1 }; // zero copies/moves
}

除了更传统的“push”/“add”风格的调用之外,标准库还经常提供“emplace”风格的调用。例如,vector::emplace 接受构造元素所需的参数,并在向量内部构造一个,而无需复制或移动任何内容。

【讨论】:

    【解决方案2】:

    最好的办法是在构造函数中就地构造a。关于二传手,没有最好的。在大多数情况下,按价值取值和移动似乎可以正常工作,但有时效率会降低。如您所示,重载效率最高,但会导致大量代码重复。模板可以在通用引用的帮助下避免代码重复,但是您必须推出自己的类型检查,这会变得复杂。除非您使用探查器检测到这是一个瓶颈,否则我建议您坚持使用 take-by-value-then-move,因为它是最简单的,可以减少代码重复并提供良好的异常安全性。

    【讨论】:

      【解决方案3】:

      经过大量研究,我找到了答案!

      我制作了一个高效的包装类,它允许你同时持有这两个选项,并让你在内部函数中决定是否要复制!

      #pragma pack(push, 1)
      template<class T>
      class CopyOrMove{
      public:
          CopyOrMove(T&&t):m_move(&t),m_isMove(true){}
          
          CopyOrMove(const T&t):m_reference(&t),m_isMove(false){}
          bool hasInstance()const{ return m_isMove; }
          const T& getConstReference() const {
              return *m_reference;
          } 
          T extract() && {
            if (hasInstance())
                  return std::move(*m_move);
            else
                  return *m_reference;
          }
          
          void fastExtract(T* out) && {
            if (hasInstance())
                  *out = std::move(*m_move);
            else
                  *out = *m_reference;
          }  
      private:
          union
          {
              T* m_move;
              const T* m_reference;
          };
          bool m_isMove;
      };
      #pragma pack(pop)
      

      现在你可以拥有这个功能了:

      void setAAndBAndCAndDAndEAndF(CopyOrMove<A> a, CopyOrMove<B> b, CopyOrMove<C> c, CopyOrMove<D> d, CopyOrMove<E> e, CopyOrMove<F> f)
      

      零代码重复!并且没有多余的复制或移动!

      【讨论】:

        【解决方案4】:

        简答:

        这是冗长和速度之间的折衷。速度不是一切。

        这样定义,有两个功能……会节省很多!

        它将节省单个移动分配,这通常不会很多。

        除非您需要让这段特定的代码尽可能快(例如,您正在编写自定义容器),否则我更喜欢按值传递,因为它不那么冗长。

        其他可能的方法是:

        • 按照其他答案中的建议,使用转发参考。它将为您提供与一对重载 (const T &amp; + T &amp;&amp;) 相同数量的副本/移动,但它使传递多个参数更容易,因为您只需要编写一个函数而不是 2N 个。

        • 使 setter 的行为类似于 emplace()。这不会给您带来任何性能优势(因为您分配给现有对象而不是创建新对象),因此没有多大意义。

        【讨论】:

          猜你喜欢
          • 2011-05-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多