如何从常量引用或通过转发模板临时构造对象

How to construct an object either from a const reference or temporary via forwarding template

本文关键字:对象 转发 常量 引用      更新时间:2023-10-16

考虑这个最小的例子

template <class T>
class Foo
{
public:
Foo(const T& t_)
: t(t_)
{
}
Foo(T&& t_)
: t(std::move(t_))
{
}
T t;
};
template <typename F>
Foo<F> makeFoo(F&& f)
{
return Foo<F>(std::forward<F>(f));
}
int main()
{
class C
{
};
C c;
makeFoo(c);
}

MSVC 2017 失败,并出现 Foo 的 ctor 的重定义错误。显然,T被推导出为C&而不是预期的C。这究竟是如何发生的,以及如何修改代码以使其执行所包含的操作:从常量引用复制构造 Foo::t 或从 r 值移动构造它。

C++17中,您可以简单地编写:

template <typename F>
auto makeFoo(F&& f)
{
return Foo(std::forward<F>(f));
}

因为类模板参数推导


C++14中,您可以编写:

template <typename F>
auto makeFoo(F&& f)
{
return Foo<std::decay_t<F>>(std::forward<F>(f));
}
template <class F, class R = std::decay_t<F>>
Foo<R> makeFoo(F&& f)
{
return Foo<R>(std::forward<F>(f));
}

这是解决问题的干净简单的方法。

衰减是将类型转换为适合存储在某处的类型的正确方法。 它对数组类型做了坏事,但在其他方面做了几乎正确的事情;无论如何,您的代码都不适用于数组类型。


编译器错误是由于引用折叠规则造成的。

X          X&          X const&       X&&
int        int&        int const&     int&&
int&       int&        int&           int&
int const  int const&  int const&     int const&&
int&&      int&        int&           int&&
int const& int const&  int const&     int const&

这些可能看起来很奇怪。

第一条规则是 const 引用是引用,但对 const 的引用是不同的。 您不能限定"参考"部分;您只能对引用的零件进行持续限定。

当你有T=int&时,当你计算T constconst T时,你只会得到int&

第二部分与如何使用 r 和 l 值引用一起工作有关。 当你做int& &&int&& &(你不能直接做;相反,你做T=int&然后T&&T=int&&T&),你总是得到一个左值引用——T&。 左值胜过右值。

然后我们添加如何推导T&&类型的规则;如果你传递一个类型为C的可变左值,你会在调用makeFoo时得到T=C&

所以你有:

template<F = C&>
Foo<C&> makeFoo( C& && f )

作为您的签名,又名

template<F = C&>
Foo<C&> makeFoo( C& f )

现在我们检查Foo<C&>. 它有两个 ctors:

Foo( C& const& )
Foo( C& && )

对于第一个,引用上的const被丢弃:

Foo( C& & )
Foo( C& && )

接下来,对引用的引用是引用,左值引用胜过右值引用:

Foo( C& )
Foo( C& )

然后我们开始,两个相同的签名构造函数。

TL;DR - 做这个答案开头的事情。

问题是提供给类的 typename 在一种情况下是引用的:

template <typename F>
Foo<F> makeFoo(F&& f)
{
return Foo<F>(std::forward<F>(f));
}

成为

template <>
Foo<C&> makeFoo(C& f)
{
return Foo<C&>(std::forward<C&>(f));
}

你可能想要一些衰败:

template <typename F>
Foo<std::decay_t<F>> makeFoo(F&& f)
{
return Foo<std::decay_t<F>>(std::forward<F>(f));
}

发生这种情况是因为引用折叠

代码中的F&&转发引用,这意味着它可以是左值引用右值引用,具体取决于它绑定到的参数的类型。

在您的情况下,如果F&&绑定到类型C&&的参数(对C的右值引用),则F简单地推导为C。但是,如果F&&绑定到类型C&的参数(如示例中所示),则引用折叠规则将确定为F推导的类型:

T&  &  -> T&
T&  && -> T&
T&& &  -> T&
T&& && -> T&&

因此,F被推导出为C&,因为C& &&坍缩为C&

您可以使用 remove_reference 从推导类型中删除任何引用:

remove_reference_t<C> -> C
remove_reference_t<C&> -> C

您可能还希望使用remove_cv来删除任何潜在的const(或volatile)限定符:

remove_cv_t<remove_reference_t<C>> -> C
remove_cv_t<remove_reference_t<C&>> -> C
remove_cv_t<remove_reference_t<C const>> -> C
remove_cv_t<remove_reference_t<C const&>> -> C

在C++20中,有一个组合的remove_cvref特征可以节省一些打字。但是,许多实现只使用decay,它执行相同的操作,但也将数组和函数类型转换为指针,根据您的用例,这可能是可取的,也可能不是可取的(标准库的某些部分已在 C++20 中从使用decay切换到使用remove_cvref)。