代码之家  ›  专栏  ›  技术社区  ›  Bérenger

完善的运输和施工人员

  •  2
  • Bérenger  · 技术社区  · 7 年前

    我试图理解完美的转发和构造器的交互作用。我的例子如下:

    #include <utility>
    #include <iostream>
    
    
    template<typename A, typename B>
    using disable_if_same_or_derived =
      std::enable_if_t<
        !std::is_base_of<
          A,
          std::remove_reference_t<B>
        >::value
      >;
    
    
    template<class T>
    class wrapper {
      public:
        // perfect forwarding ctor in order not to copy or move if unnecessary
        template<
          class T0,
          class = disable_if_same_or_derived<wrapper,T0> // do not use this instead of the copy ctor
        > explicit
        wrapper(T0&& x)
          : x(std::forward<T0>(x))
        {}
    
      private:
        T x;
    };
    
    
    class trace {
      public:
        trace() {}
        trace(const trace&) { std::cout << "copy ctor\n"; }
        trace& operator=(const trace&) { std::cout << "copy assign\n"; return *this; }
        trace(trace&&) { std::cout << "move ctor\n"; }
        trace& operator=(trace&&) { std::cout << "move assign\n"; return *this; }
    };
    
    
    int main() {
      trace t1;
      wrapper<trace> w_1 {t1}; // prints "copy ctor": OK
    
      trace t2;
      wrapper<trace> w_2 {std::move(t2)}; // prints "move ctor": OK
    
      wrapper<trace> w_3 {trace()}; // prints "move ctor": why?
    }
    

    我想要我的 wrapper 完全没有开销。尤其是在将临时数据编组到包装器中时,如 w_3 ,我希望 trace 对象将直接就地创建,而不必调用移动ctor。但是,有一个move ctor调用,这使我认为创建了一个临时的,然后从中移出。为什么调用移动ctor?怎么不叫它?

    2 回复  |  直到 7 年前
        1
  •  5
  •   bolov    7 年前

    我希望直接在适当的位置创建跟踪对象, 无需调用移动ctor。

    我不知道你为什么这么想。转发就是这样:移动或复制 1) . 在示例中,创建一个临时 trace() 然后转发将其移动到 x

    如果要构造 T 对象就位,然后需要将参数传递给 T 不是一个 T 要移动或复制的对象。

    创建就地构造函数:

    template <class... Args>
    wrapper(std::in_place_t, Args&&... args)
        :x{std::forward<Args>(args)...}
    {}
    

    然后这样称呼它:

    wrapper<trace> w_3 {std::in_place};
    // or if you need to construct an `trace` object with arguments;
    wrapper<trace> w_3 {std::in_place, a1, a2, a3};
    

    就另一个答案发表意见:

    @博洛夫让我们忘记完美的转发一分钟。我认为 问题是,我希望在最后一个时候构造一个对象 目的地。现在,如果它不在构造函数中,则它现在被担保 使用担保的复制/移动省略发生(此处移动和复制是 几乎一样)。我不明白的是为什么 在构造函数中是可能的。我的测试案例证明它没有发生 按照目前的标准,但我认为这不应该是 不能由标准规定,也不能由编译程序实现。什么 我想那是不是很特别?

    在这方面,一个系数绝对没有什么特别之处。使用简单的自由函数可以看到完全相同的行为:

    template <class T>
    auto simple_function(T&& a)
    {
        X x = std::forward<T>(a);
        //  ^ guaranteed copy or move (depending on what kind of argument is provided
    }
    
    auto test()
    {
        simple_function(X{});
    }
    

    上面的示例与您的操作类似。您可以看到 simple_function 类似于您的包装构造函数和我的本地 X 变量模拟数据成员 wrapper . 这方面的机制是相同的。

    为了理解为什么不能直接在 单纯形函数 (或者作为您的包装对象中的数据成员),您需要了解如何在C++ 17中推荐如何保证复制删除工作。 this excelent answer .

    总结一下答案:基本上,prvalue表达式并不具体化对象,而是可以初始化对象。在使用表达式初始化对象之前,请尽可能长时间地保存该表达式(这样可以避免一些复制/移动)。请参阅链接的答案,以获得更深入而友好的解释。

    使用表达式初始化 simple_foo (或构造函数的参数)强制实现对象并丢失表达式。从现在开始,您不再拥有原始的prvalue表达式,而是拥有一个已创建的物化对象。现在需要将这个对象移动到您的最终目的地-我的本地 X (或您的数据成员) X )

    如果我们稍微修改一下我的示例,我们可以看到工作中有保证的副本删除:

    auto simple_function(X a)
    {
        X x = a;
        X x2 = std::move(a);
    }
    
    
    auto test()
    {
        simple_function(X{});
    }
    

    如果没有省略,事情会是这样的:

    • X{} 创建临时对象作为的参数 单纯形函数 . 让我们称之为 Temp1
    • TEMP1 现在移动到参数中(因为它是prvalue) a 属于 单纯形函数
    • 被复制(因为 是左值)到 X
    • 被移动(因为 std::move 铸件 到xvalue)到 x2

    现在用C++ 17保证拷贝删除

    • x{} 不再物质主义的对象在现场。相反,表达式保持不变。
    • 参数 属于 单纯形函数 现在可以通过初始化 x{} 表达式。无需复制或移动。

    其余的现在是一样的:

    • 被复制到 x1
    • 被搬进 X2

    你需要理解的是:一旦你命名了某件事,那件事 必须 存在。令人惊讶的简单原因是,一旦你有了某个东西的名字,你可以多次引用它。看看我的答案 other question . 您已将参数命名为 wrapper::wrapper . 我已将参数命名为 单纯形函数 . 这是您丢失prvalue表达式初始化该命名对象的时刻。


    如果你想使用C++ 17保证拷贝删除,你不喜欢就地方法,你需要避免命名事物:)你可以用lambda来做。我经常看到的习惯用法,包括在标准中,都是现成的。因为我在野外还没有见过lambda,所以我不知道是否会推荐它。不管怎样,这里是:

    template<class T> class wrapper {
    public:
    
        template <class F>
        wrapper(F initializer)
            : x{initializer()}
        {}
    
    private:
        T x;
    };
    
    auto test()
    {
        wrapper<X> w = [] { return X{};};
    }
    

    在C++ 17中,它不授予拷贝和/或移动,即使它工作。 X 已删除复制构造函数和移动构造函数。这个对象将在它的最终目的地构建,就像您想要的那样。


    1) 我说的是转发成语,如果使用得当。 std::forward 只是一个演员。

        2
  •  0
  •   xskxzr    7 年前

    引用(左值引用或右值引用)必须绑定到对象,因此当引用参数 x 初始化时,无论如何都需要实现临时对象。在这个意义上,完美的转发并不是“完美的”。

    从技术上讲,要消除这种移动,编译器必须同时知道初始值设定项参数和构造函数的定义。这是不可能的,因为它们可能位于不同的翻译单元中。

    推荐文章