【问题标题】:Generic constructor template called instead of copy/move constructor调用通用构造函数模板而不是复制/移动构造函数
【发布时间】:2022-01-12 23:20:01
【问题描述】:

我设计了一个更简单的包装类,它为对象添加标签,目的是隐式转换/能够替换被包装的对象。

#include <string>
#include <type_traits>
#include <utility>

template < typename T, typename Key = std::string >
class myTag{
    T val;
 public:
    Key key;

    template < typename... Args,
               typename = typename std::enable_if< std::is_constructible< T, Args... >::value >::type >
    myTag(Args&&... args) :
        val(std::forward< Args >(args)...) {
            std::cout << "forward ctor" << std::endl;
    }

    myTag(const myTag& other) :
        key(other.key), val(other.val) {
            std::cout << "copy ctor" << std::endl;
    }

    myTag(myTag&& other):
        key(other.key), val(other.val) {
            std::cout << "move ctor" << std::endl;
    }

    operator T&() { return val; }

    operator const T&() const { return val; }
};


int main(int argc, char const *argv[]) {
    myTag< float > foo(5.6);   // forward ctor
    myTag< float > bar(foo);   // forward ctor

    return 0;
}

但是,我无法正确声明和定义复制/移动构造函数。我声明了一个通用构造函数重载模板,该模板将其参数转发给底层类型,只要这种构造是可能的。但是,由于隐式转换运算符,它会捕获 myTag 的每个实例化,从而有效地隐藏复制/移动构造函数。

非默认复制/移动语义的要点是复制/移动 key 值与使用构造函数模板默认初始化它。

如何让编译器优先考虑/优先考虑显式复制/移动构造函数与泛型重载?是否有任何其他 SFINAE 检查替代 is_constructible&lt;&gt; 可以避免隐式转换?

编辑:我应该补充一点,我正在寻找 C++14 解决方案。

【问题讨论】:

  • 这需要一个比我更好的语言律师(建议你添加那个标签)来告诉你为什么,但你也可以通过添加myTag(myTag&amp; other) 重载来解决它。问题是 foo 不是myTag&amp;&amp;,也不是const myTag&amp;,所以编译器选择通过operator T&amp; 转换并使用第一个构造函数。
  • 如果只有 1 个参数,并且该参数是 myTag(从类型中删除 cv 和引用),您还可以扩展 enable_if 以禁止此重载。
  • @DaveS 是的,可以。我只是不明白为什么foo 不是const myTag&amp;,或者更确切地说,为什么const myTag&amp; 不绑定到它而不是编译器寻找替代重载。
  • @joaocandre:我认为您感到困惑的一点是myTag&amp; 在重载选择期间未绑定到const myTag&amp;。在重载选择期间,它首先寻找完全匹配,以及需要某种转换的匹配(例如myTag&amp;const myTag&amp;)。因此,由于您的转发构造函数不需要转换,除非其他重载之一完全匹配,否则它将被选中。
  • @DaveS 但这正是我的观点 - 前向构造函数 应该 需要转换,因为 float 不能从 myTag 参数的任何变体中构造 - 就我而言知道它正在调用隐式转换重载operator T&amp; 并复制到新实例中。对于myTag&amp;const myTag&amp; 或类似的,SFINAE 检查应该(理想情况下)失败,因此应该调用复制/移动构造函数。

标签: c++ templates variadic-templates sfinae constructor-overloading


【解决方案1】:

它不会影响复制和移动构造函数。在某些情况下,它只是在重载解决方案中击败了它们。

  • 如果传递myTag&lt;float&gt;&amp;,则使用转发构造函数。
  • 如果您传递 const myTag&lt;float&gt;&amp;,则使用复制构造函数。
  • 如果传递myTag&lt;float&gt;&amp;&amp;,则使用移动构造函数。
  • 如果传递const myTag&lt;float&gt;&amp;&amp;,则使用转发构造函数。

复制和移动构造函数不是模板,因此它们将胜过具有相同签名的模板。但是在推导出的签名与复制和移动构造函数不同的情况下,转发构造函数可以更好地匹配。

处理这个问题的惯用方法是根据参数的衰减类型从考虑中删除转发构造函数:

template <typename... Args>
constexpr myTag(Args&&... args)
    requires
        ((sizeof...(Args) != 1 && std::is_constructible_v<T, Args...>) ||
         (sizeof...(Args) == 1 && std::is_convertible_v<Args..., T> && !std::is_same_v<myTag, std::decay_t<Args>...>))

【讨论】:

  • 谢谢。这显然是有原因的 - 但我确实认为重载决议宁愿执行到T的隐式转换而不是将myTag&amp;绑定到const myTag&amp;是很奇怪的
  • 另外,我应该补充一点,我正在寻找 C++14 解决方案。
  • 并不是更喜欢隐式转换。就是这样,因为满足了可转换性约束,所以该构造函数可用(而不是 SFINAE)。该构造函数推导出一个非常匹配的参数 (myTag&amp;)。因此重载决议选择将myTag&amp; 参数绑定到myTag&amp; 而不是const myTag&amp;。问题在于,根据设计,转发引用通常是非常好的匹配。无需转换。
  • (对于 C++14,只需将布尔表达式放在 enable_if 中,而不是在 requires 子句中。)
猜你喜欢
  • 1970-01-01
  • 2022-11-21
  • 2015-06-10
  • 1970-01-01
  • 1970-01-01
  • 2013-04-25
  • 2021-12-25
  • 2016-07-12
相关资源
最近更新 更多