由于对模板化类型的通用(前向)引用而无法实例化函数模板

A failure to instantiate function templates due to universal (forward) reference to a templated type

Universal references (i.e. "forward references", the c++ standard name) and perfect forwarding in c++11, c++14, and beyond have many important advantages; see here, and here.

在上面引用的 Scott Meyers 的文章 (link) 中,根据经验规定:

If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.

示例 1

的确,使用 clang++ 我们看到以下代码片段将成功编译 -std=c++14:

#include <utility>

template <typename T>
decltype(auto) f(T && t)
{
    return std::forward<T>(t);
}

int        x1 = 1;
int const  x2 = 1;
int&       x3 = x1;
int const& x4 = x2;

// all calls to `f` result in a successful
// binding of T&& to the required types
auto r1 = f (x1);    // various lvalues okay, as expected
auto r2 = f (x2);    // ...
auto r3 = f (x3);
auto r4 = f (x4);
auto r5 = f (int()); // rvalues okay, as expected

鉴于对通用引用(前向引用)和类型推导的任何描述(例如,参见 this explanation),很明显为什么上面的方法有效。虽然,从同样的解释来看,为什么下面的方法也不起作用还不是很清楚。

(失败)示例 2

This question 解决了同样的问题。但是,所提供的答案并未解释为什么模板化类型未归类为 "deduced".

我要展示的(貌似)满足了迈耶斯的上述要求。但是,以下代码截断了 编译失败 ,产生了错误(以及对 f 的每次调用):

test.cpp:23:11: error: no matching function for call to 'f'

auto r1 = f (x1);

test.cpp:5:16: note: candidate function [with T = foo, A = int] not viable: no known conversion from 'struct foo< int >' to 'foo< int > &&' for 1st argument

decltype(auto) f (T< A > && t)

#include <utility>

//
// It **seems** that the templated type T<A> should
// behave the same as an bare type T with respect to
// universal references, but this is not the case.
//
template <template <typename> typename T, typename A>
decltype(auto) f (T<A> && t)
{
    return std::forward<T<A>> (t);
}

template <typename A>
struct foo
{
    A bar;
};

struct foo<int>        x1 { .bar = 1 };
struct foo<int> const  x2 { .bar = 1 };
struct foo<int> &      x3 = x1;
struct foo<int> const& x4 = x2;

// all calls to `f` **fail** to compile due
// to **unsuccessful** binding of T&& to the required types
auto r1 = f (x1);
auto r2 = f (x2);
auto r3 = f (x3);
auto r4 = f (x4);
auto r5 = f (foo<int> {1}); // only rvalue works

在上下文中,由于f的参数的类型T<A>被推导为,所以肯定参数声明 T<A>&& t 将表现为通用引用(前向引用)。

示例 3(为了清楚地描述手头的问题)

让我强调以下几点:Example 2 中的代码编译失败是 而不是 因为 struct foo<> 是一个模板类型.失败似乎是 f 的参数声明为模板类型引起的。

考虑对先前代码的以下修改,现在 编译:

#include <utility>

//
// If we re-declare `f` as before, where `T` is no longer a
// templated type parameter, our code works once more.
//
template <typename T>
decltype(auto) f (T && t)
{
    return std::forward<T> (t);
}

//
// Notice, `struct foo<>` is **still** a templated type.
//
template <typename A>
struct foo
{
    A bar;
};

struct foo<int>        x1 { .bar = 1 };
struct foo<int> const  x2 { .bar = 1 };
struct foo<int> &      x3 = x1;
struct foo<int> const& x4 = x2;

// all calls to `f` (again) result in
// a successful binding of T&& to the required types
auto r1 = f (x1);
auto r2 = f (x2);
auto r3 = f (x3);
auto r4 = f (x4);

令我惊讶的是,这个简单的更改完全改变了模板函数 f 的类型参数的类型推导行为。

问题:

为什么第二个例子没有按预期工作?在 c++11/14 中是否有技术可以解决模板化类型的这个问题?是否有众所周知的现存代码库(在野外)成功地使用 c++ 的模板化类型的前向引用?

当你用一些左值调用函数f时:

int a = 42;
f(a);

那么f肯定能接受这样的左值。当 f 的第一个参数是(左值)引用类型,或者根本不是引用时就是这种情况:

auto f(int &);
auto f(int); // assuming a working copy constructor

当参数是右值引用时,不会工作:

auto f(int &&); // error

现在,当您像在第一个和第三个示例中那样定义一个将转发引用作为第一个参数的函数时...

template<typename T>
auto f(T&&); // Showing only declaration

... 你实际上用一个左值调用这个函数,模板类型推导将 T 变成一个(左值)引用(这种情况可以在我稍后提供的示例代码中看到) :

auto f(int & &&); // Think of it like that

当然,上面涉及的参考太多了。所以C++有collapsing rules,其实很简单:

  • T& & 变为 T&
  • T& && 变为 T&
  • T&& & 变为 T&
  • T&& && 变为 T&&

由于第二条规则,f 的第一个参数的 "effective" 类型是左值引用,因此您可以将左值绑定到它。

现在当你定义一个函数时 g 就像...

template<template<class> class T, typename A>
auto g(T<A>&&);

那么无论如何,模板参数推导必须T变成模板不是一个类型。毕竟,在将模板参数声明为 template<class> class 而不是 typename 时,您已经明确指定了这一点。 (这是一个重要的区别,foo 在你的例子中是 不是 类型,它是一个模板......你可以将其视为类型级别的函数,但回到主题)

现在,T 是某种模板。您不能引用模板。 引用(类型)是从(可能不完整的)type 构建的。所以无论如何,T<A>(它是一种类型,但不是可以推导的模板参数)不会变成(左值)引用,这意味着 T<A> && 不需要任何折叠并保持原样:右值引用。当然,您不能将左值绑定到右值引用。

但是如果你给它传递一个右值,那么即使 g 也可以。

以上所有内容都可以在以下示例中看到:

template<typename X>
struct thing {
};
template<typename T>
decltype (auto) f(T&& t) {
 if (std::is_same<typename std::remove_reference<T>::type, T>::value) {
  cout << "not ";
 }
 cout << "a reference" << endl;
 return std::forward<T>(t);
}
template<
 template<class> class T,
 typename A>
decltype (auto) g(T<A>&& t) {
 return std::forward<T<A>>(t);
}
int main(int, char**) {
 thing<int> it {};

 f(thing<int> {}); // "not a reference"

 f(it);            // "a reference"
 // T = thing<int> &
 // T&& = thing<int>& && = thing<int>&

 g(thing<int> {}); // works

 //g(it);
 // T = thing
 // A = int
 // T<A>&& = thing<int>&&

 return 0;
}

(Live here)

关于如何"overcome":你不能。至少不是你想要的方式,因为自然的解决方案是你提供的第三个例子:因为你不知道传递的类型(它是左值引用,右值引用还是引用完全没有?)你必须保持它像 T 一样通用。你当然可以提供重载,但我想这会以某种方式破坏完美转发的目的。


嗯,事实证明你实际上 可以 克服这个问题,使用一些特征 class:

template<typename> struct traits {};
template<
 template<class>class T,
 typename A>
struct traits<T<A>> {
 using param = A;
 template<typename X>
 using templ = T<X>;
};

然后您可以提取模板和在函数内部实例化模板的类型:

template<typename Y>
decltype (auto) g(Y&& t) {
 // Needs some manual work, but well ...
 using trait = traits<typename std::remove_reference<Y>::type>;
 using A = typename trait::param;
 using T = trait::template templ
 // using it
 T<A> copy{t};
 A data;
 return std::forward<Y>(t);
}

(Live here)


[...] can you explain why it is not an universal reference? what would the danger or the pitfall of it be, or is it too difficult to implement? I am sincerely interested.

T<A>&& 不是通用引用,因为 T<A> 不是模板参数。它是(在扣除 TA 之后)一个简单的(固定/非通用)类型。

将其设为转发引用的一个严重缺陷是您无法再表达 T<A>&& 的当前含义:对从模板 T 构建的某种类型的右值引用,参数 A.

Why does the second example not work as expected?

您有两个签名:

template <typename T>                                                                                                                   
decltype(auto) f (T&& );

template <template <typename> typename T, typename A>                                                                                                                   
decltype(auto) f2 (T<A>&& );

f 采用转发引用,但 f2 不采用。具体规则,来自[temp.deduct.call],大胆强调我的:

A forwarding reference is an rvalue reference to a cv-unqualified template parameter. If P is a forwarding reference and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.

对于 f,参数是对模板参数 (T) 的右值引用。但是对于 f2T<A> 不是模板参数。因此,该函数仅将对 T<A> 的右值引用作为其参数。这些调用无法编译,因为您的所有参数都是左值,并且这种情况下没有使用左值调用的特殊例外。

至于克服这个问题,我想一个或多或少等效的方法是用前向引用推导它,然后手动触发与 T<A> 的比较。

template<typename T>
class X;

template<template<typename> class T, typename A>
class X<T<A>> {
public:
   using type = A;

   template<typename _>
   using template_ = T<_>;
};

template<typename T, typename R>
struct copyref {
  using type = T;
};
template<typename T, typename R>
struct copyref<T, R&> {
   using type = T&;
};
template<typename T, typename R>
struct copyref<T, R&&> {
   using type = T&&;
};

template <typename U, typename XX = X<std::decay_t<U>>, 
          typename = typename XX::type >                                                                                                                   
decltype(auto) f (U && t)                                                                                                               
{                                                                                                                                      
    return std::forward<
       typename copyref<
         typename XX::template template_<typename XX::type>, U
       >::type>(t);
}

如果您实际上不需要 T<A> 而是特定类型,最好的方法是使用 std::enable_if_t<std::is_same_v<std::decay_t<U>, SpecificType>>,我想这样更容易。

int main() {
    static_assert(std::is_same<decltype(f(std::declval<X<int>&>())), 
                  X<int>&>::value, "wrong");
    static_assert(std::is_same<decltype(f(std::declval<X<int>>())), 
                  X<int>&&>::value, "wrong");
    // compile error
    //f(std::declval<int>());
}

仅有类型推导是不够的。类型声明的形式必须 完全 T&&(仅对 模板参数 的右值引用)。如果不是(或者没有类型推导),则参数是右值引用。如果参数是左值,它将无法编译。由于 T<A>&& 没有这种形式,因此 f (T<A> && t) 无法接受左值(作为左值引用),您会收到错误消息。如果您认为这需要太多的普遍性,请考虑一个简单的 const 限定符也打破它:

template<typename T>
void f(const T&& param); // rvalue reference because of const

(撇开 const 右值引用的相对无用)

除非使用最通用的形式 T&&,否则引用折叠规则根本不会生效。如果 f 无法识别传递的 lvalue 参数并将该参数视为 lvalue 引用 ,则没有引用折叠待完成(即将 T& && 折叠为 T& 不会发生,它只是 T<something>&&,一个模板类型的右值引用)。函数确定右值还是左值作为参数传递所需的机制编码在推导的模板参数中。但是,这种编码仅适用于严格定义的通用参考参数。

为什么这种普遍性是必要的(除了作为规则之外)? 如果没有这种特定的定义格式,通用引用就无法成为实例化以捕获任何类型参数的超级贪婪函数……正如它们设计的那样。 获取重点,我认为:假设您要定义一个函数,该函数具有对模板化类型参数的常规右值引用,T<A>&&(即不接受左值参数)。如果以下语法被视为通用引用,那么您将如何更改它以指定常规右值引用?

template <template <typename> typename T, typename A>
decltype(auto) f (T<A> && t) // rv-ref - how else would you exclude lvalue arguments?

需要有一种方法将参数显式定义为右值引用以排除左值参数。这个推理似乎适用于其他类型的参数,包括 cv 限定.

此外,似乎有解决此问题的方法(请参阅 traits 和 SFINAE),但我无法回答该部分。 :)