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

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

考虑这个最小的例子

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 构造函数的重新定义错误。显然 T 被推断为 C& 而不是预期的 C。这究竟是如何发生的以及如何修改代码以使其执行预期的操作:从 const 引用复制构造 Foo::t 或从 r 移动构造它-值。

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 的引用是不同的。您不能限定 "reference" 部分;您只能对引用的部分进行 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&>。它有两个演员:

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

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

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

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

Foo( C& )
Foo( C& )

好了,两个相同的签名构造器。

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

问题是提供给 class 的类型名在一种情况下是参考:

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));
}

C++17中你可以简单地写:

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

因为class template argument deduction.


C++14中你可以这样写:

template <typename F>
auto 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 trait which can save some typing. However, many implementations just use decay,它做同样的事情,但也将数组和函数类型转换为指针,这可能需要也可能不需要,具体取决于您的用例 (在 C++20 中,标准库的某些部分已从使用 decay 切换到使用 remove_cvref