C++11中的可变通用引用构造函数和复制构造函数

8
如果我们有带通用引用参数的构造函数,如何声明复制构造函数? http://coliru.stacked-crooked.com/a/4e0355d60297db57
struct Record{
    template<class ...Refs>
    explicit Record(Refs&&... refs){
        cout << "param ctr" << endl;
    }

    Record(const Record& other){     // never called
        cout << "copy ctr" << endl;
    }

    Record(Record&& other){         // never called
        cout << "move ctr" << endl;
    }    
};

int main() {
    Record rec("Hello");    
    Record rec2(rec);  // do "param ctr"

    return 0;
}

根据std::tuple的构造函数列表http://en.cppreference.com/w/cpp/utility/tuple/tuple [请看case 3和8],标准库中已经以某种方式解决了这个问题...但我无法理解stl的代码。

P.S. 这个问题与C++通用引用在构造函数和返回值优化中的应用有些关联。

P.P.S. 目前,我只是添加了额外的第一个参数Record(call_constructor, Refs&&... refs)来进行显示调用。我可以手动检测是否只有一个参数且为Record,然后将调用重定向到复制构造函数/参数构造函数,但...我不敢相信没有标准的方法来解决这个问题...


2
我无法写出完整的答案,但这个链接可能会对你有所帮助:https://akrzemi1.wordpress.com/2013/10/10/too-perfect-forwarding/ 基本思路是使用SFINAE。 - Daniel Jour
@DanielJour 这与我的 P.P.S. 部分有些相似,但感谢,很高兴知道这是已知的问题... - tower120
为了未来证明这个问题,应该指出通用引用将正式被称为转发引用。该提案可以在这里找到。 - huu
3个回答

3
在你的例子中,使用了转发引用和Record&一起使用。因此,你可以为Record&添加额外的重载(以转发到复制构造函数)。
Record(Record& other) : Record(static_cast<const Record&>(other)) {}

或者在具有转发引用的函数模板上使用SFINAE技术。

手动使用 const_cast 有什么缺点吗?移动构造函数呢? - tower120
@tower120:移除const可能是危险的,我看到唯一的副作用是使用另一个重载(如此处所示)。 - Jarod42
@tower120:移动构造函数可能已经被调用,如果需要的话,您可以添加额外的const Record &&重载。 - Jarod42
顺便说一句,您可能更喜欢使用 static_cast 来添加 const - 为什么? - tower120
@tower120:请查看const-cast-vs-static-cast - Jarod42
那么,请使用更“安全”的变体更新答案 :) - tower120

2
问题 当您调用 Record rec2(rec); 时,您有两个可行的构造函数:您的复制构造函数 Record(Record const&) 和具有 Refs = {Record&} 的可变参数构造函数,它相当于 Record(Record&)。后者是更好的候选项,因为它是一个不太限定的引用,所以即使这不是您想要的,它也会胜出。 解决方案 您需要删除任何应调用移动或复制构造函数的内容,使其成为可变参数构造函数的可行候选项。简单来说,如果 Refs... 包含一个类型,该类型是从 Record 派生的引用或普通值,则我们不想使用可变参数构造函数。还包括派生情况,因为您肯定希望 SpecialRecord sr; Record r(sr); 调用复制构造函数...
由于这经常出现,因此将其作为类型特征非常有用。基本情况是既不是复制也不是移动:
template <typename T, typename... Ts>
struct is_copy_or_move : std::false_type { };

我们只需要专注于一种类型:
template <typename T, typename U>
struct is_copy_or_move<T, U>
: std::is_base_of<T, std::decay_t<U>>
{ }

然后我们只需要用这个SFINAE的替代方法来替换我们的可变参数构造函数:

template <typename... Refs,
          typename = std::enable_if_t<!is_copy_or_move<Record, Refs...>::value>
          >
Record(Refs&&...);

如果参数表明应该调用复制或移动构造函数,可变参构造函数将不再可行。


1

在编程中,过度使用转发引用是一种不好的做法(参见《Effective modern C++》第26项)。由于重载解析规则,它们倾向于吞噬你传递给它们的所有内容。

在您的示例中,您正在使用非const Record对象构造Record对象,这就是为什么您的复制构造函数没有被执行的原因。如果您像this这样调用它

Record rec2(const_cast<Record const&>(rec));

然后它按预期工作。
解决方案是在具有转发引用的构造函数上进行SFINAE,并禁用应调用复制构造函数的情况;但在可变参数的情况下,编写起来会有些丑陋:
template <
    class Ref1, class ...Refs, 
    typename = typename std::enable_if <
        !std::is_same<Ref1, Record&>::value || sizeof...(Refs)
    >::type
>
explicit Record(Ref1&& ref, Refs&&... refs)
{
    cout << "param ctr" << endl;
}

现在正在调用。
 Record rec2(rec); // calls copy ctor 

由于模板无法实例化为Record&,因此调用复制构造函数。

演示


如果你发现自己经常这样做(不推荐),你可以通过定义类型特征来执行SFINAE并消除一些混乱。
template<class T1, class T2, class... Refs>
using no_copy_ctor = typename std::enable_if <
    !std::is_same<T1, T2>::value || sizeof...(Refs)>::type;

因此,将上述内容编写为:

template<class Ref1, class ...Refs, typename = no_copy_ctor<Record&, Ref1, Refs...>>
explicit Record(Ref1&& ref, Refs&&... refs)
{ /*...*/ }

而且条件实际上更加复杂,你忘记检查 sizeof...(Refs) == 0 - Jarod42
@Jarod42 你假设 OP 想要将可变参数构造函数用作零参数构造函数。 - Nikos Athanasiou
目前 OP 的代码允许零参数构造函数。这只是一个注释,很容易修复。 :-) - Jarod42
1
对于大小,我的意思是你也禁止 Record rec2(rec, foo); - Jarod42
is_same 不是一个足够的检查。在应该优先使用复制/移动构造函数的情况下,您仍然会发现自己更喜欢可变参数构造函数。 - Barry
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接