【问题标题】:How to guarantee copy elision with std::variant?如何使用 std::variant 保证复制省略?
【发布时间】:2019-06-04 00:12:19
【问题描述】:

我有这种类型:

struct immobile {
   // other stuff omitted
   immobile(immobile&) = delete;
   immobile(immobile&&) = delete;
};
immobile mk_immobile();
// e.g. this compiles
// mk_immobile() is a prvalue and i is its result object
immobile i(mk_immobile());

我也有这个类模板:

template<typename T>
struct container {
    std::variant<T, other_stuff> var;
    template<typename... Args>
    container(Args&&... args)
    : var(std::in_place_index<0>, std::forward<Args>(args)...) {}
};

我想围绕mk_immobile() 生成的对象构造一个container,其中immobile 对象用于初始化var 的变体之一。

container<immobile> c(mk_immobile());

但是,这不起作用。一方面,std::variant 的构造函数需要std::is_constructible_v&lt;immobile, immobile&gt;,但它不成立。更糟糕的是,即使是这个简化版本也失败了:

template<typename T>
struct demonstration {
    T t;
    template<typename... Args>
    demonstration(Args&&... args) : t(std::forward<Args>(args)...) {}
};
demonstration<immobile> d(mk_immobile());

这似乎暗示std::forward 实际上并不完全转发——prvalues 不会作为prvalues 转发。 (这对我来说很有意义;我认为这样做是不可能的。)我可以通过将 demonstration 更改为这样来使它工作:

template<typename T>
struct demonstration {
    T t;
    template<typename F>
    demonstration(F&& f) : t(std::forward<F>(f)()) {}
};
demonstration<immobile> d([] { return mk_immobile(); });

但我看不到以类似方式更改 container 的方法。如何更改container 以便它可以从prvalue 构造std::variant(或其他标记的联合)?我可以更改container,但不能更改immobile

【问题讨论】:

  • 通过快速测试:gcc.godbolt.org/z/27cALe 似乎应该可以通过使用基于延迟回调的构造思想来实现一个按照您想要的方式运行的标记联合(我只是做了很多同一事物的更简单版本)

标签: c++ c++17 variant copy-elision


【解决方案1】:

你滥用演员表

template<typename F>
struct initializer
{
    F f;
    template<typename T>
    operator T()
    {
        return f();
    }
};

template<typename F>
initializer(F&&) -> initializer<F>;

并用作

container<immobile> c{initializer{[]{
    return mk_immobile();
}}};

【讨论】:

  • 我喜欢!但是,我认为initializer 可能需要进行两次更改才能使其更完美地向前推进。我认为F f 应该是F&amp;&amp; ff() 应该是std::forward&lt;F&gt;(f)()。函子可以使用完美转发,因为operator() 可以同时出现在&amp;-qualified 和&amp;&amp;-qualified 版本中,并且转发允许更合适的重载解决方案。 initializers 只能使用一次,所以我认为让函子自行移动是安全的。见this example。这种改变是个好主意吗?
  • @HTNW 我认为这不是一个好主意。您正在增加意外创建悬空引用的风险,只是为了明确地进行任何编译器本身都会做的优化。
  • operator() &amp;operator() &amp;&amp; 可能不同;我不认为这种变化是一种优化。我实际上可以使用v_initializer { [] { return std::forward&lt;F&gt;(f)(); } }(@PasserBy 发布的initializer)来模拟fw_initializer { std::forward&lt;F&gt;(f)(); }(我修改的initializer)的行为。我认为您不会意外地调用移动语义;您需要同时使用 std::move 并编写自定义 lambda 或使用 std::forward
  • @HTNW F&amp;&amp; f 几乎可以肯定是一个错误,现在只要在其完整表达式之外使用initializer,您就会有一个悬空引用。即使没有参考,您也可以转发f
  • 好的,总结一下:如果我有F f,其中F 是不动的并且想要用f() 构建的container,我不能用f 构建你的initializer在其中,但我可以捕获 f 作为 lambda 中的引用或使 initializer 持有引用(&amp;&amp;&amp; — 显然没有区别)本身。我需要 a 引用,如果f 被破坏,悬空引用是不可避免的。但是,您的 initializer 不允许意外使用 f 的临时值,从而删除此类引用的一个可能来源。感谢您的耐心等待。
猜你喜欢
  • 2016-10-28
  • 1970-01-01
  • 2020-12-13
  • 2021-08-21
  • 2023-01-30
  • 1970-01-01
  • 2021-10-26
  • 1970-01-01
相关资源
最近更新 更多