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


普遍参考 (即“前向引用”) c++ 标准名称)和完美的转发 c++11c++14以及其他方面有许多重要的优点;看到 这里,和 这里

在上面引用的Scott Meyers的文章中(链接),根据经验表明:

如果声明变量或参数具有类型 Ť&& 对于一些 演绎类型 T,该变量或参数是通用参考。

例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

给出通用引用(前向引用)和类型推导的任何描述(例如,参见 这个解释)很清楚为什么上述工作。虽然,从同样的解释来看,为什么以下也不能起作用并不是很清楚。

(失败)例2

这个问题 解决了同样的问题。但是,提供的答案并不能解释为什么模板化类型不被归类为“推断”。

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

test.cpp:23:11:错误:调用'f'没有匹配函数

auto r1 = f(x1);

test.cpp:5:16:注意:候选函数[有T = foo,A = int]没有   可行:没有已知的从'struct foo <int>'到'foo <int> &&'的转换   第一个论点

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

在上下文中,因为类型 T<A> 的 f的参数  推断,肯定是参数声明 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++带有模板类型的前向引用?


2592
2017-08-29 04:30


起源

只有T &&作为转发参考。 T <A> &&是右值参考。 X都是左值 - Piotr Skotnicki
是的,在这个例子中很清楚 T<A>&& 正被编译器视为右值引用。这正是为什么示例2中的代码无法编译的原因。这没有回答 为什么  T<A>&& 不能用作前向参考;即,手头的问题。 - Dalton
不可能从专用模板类型中提取A,因为它是未知的。如果您需要A,您需要将其作为模板参数传递或从T中提取。 T::type - Simon Kraemer


答案:


当你调用某个功能时 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 ++有 崩溃的规则,其实很简单:

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

感谢第二个规则,第一个参数的“有效”类型 f 是左值引用,因此您可以将左值绑定到它。

现在定义一个函数 g 喜欢 ...

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

然后无论如何,模板参数扣除 必须 关闭 T 变成一个 模板 一种。毕竟,在将模板参数声明为时,您确切地指定了 template<class> class 代替 typename。 (这是一个重要的区别, foo 在你的例子中  一个类型,它是一个模板......你可以看到它作为类型级别函数,但回到主题)

现在, T 是某种模板。您不能引用模板。 引用(类型)是从(可能不完整)构建的 类型。无论如何, 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;
}

住在这里

关于如何“克服”这个:你不能。至少不是你想要它的方式,因为 自然解决方案是您提供的第三个示例:由于您不知道传递的类型(它是左值引用,右值引用还是引用?),您必须将其保持为通用的 T。你当然可以提供重载,但我想这会以某种方式破坏完美转发的目的。


嗯,实际上是你 能够 克服这一点,使用一些特征类:

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

住在这里


[...] 能够  解释为什么它不是一个普遍的参考?它的危险或陷阱是什么,或者实施起来太难了?我真的很感兴趣。

T<A>&& 不是一个普遍的参考因素 T<A> 不是模板参数。这是(在扣除两者之后) T 和 A)简单(固定/非通用)类型。

使这个转发参考的一个严重缺陷是你不能再表达当前的含义 T<A>&&:对从模板构建的某种类型的右值引用 T 带参数 A


7
2017-08-29 06:49



对于downvoters:请解释为什么你认为我的答案不是一个好/可接受的答案。 - Daniel Jour
因为这个答案没有回答这个问题:为什么不回答 T<A>&& 转发参考。原因与参考折叠规则无关。你的答案只是一堵无关紧要的文字。 - Barry
我不同意: T<A>&& 不能转发参考因为 T<A> 部分在任何情况下都不能推断为参考类型(仅作为参考类型) T 和 A 可以推断)。我故意遗漏了标准,因为恕我直言的措辞没有很大帮助,这不是一个“语言律师”的问题。 - Daniel Jour
@barry,但可以 您 解释为什么它不是一个普遍的参考?它的危险或陷阱是什么,或者实施起来太难了?我真的很感兴趣。 - Johannes Schaub - litb
@Daniel很好!为什么T <A> &&不能成为左值参考?我希望您将最后的评论复制到您的答案中,并删除其他文本墙。这句话比巴里的所有规则背诵更有价值,而实际上并没有解释其原因。 - Johannes Schaub - litb


为什么第二个例子不能按预期工作?

你有两个签名:

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

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

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

一个 转发参考 是一个左值   参考cv-不合格 模板参数。如果P是转发引用且参数是a   左值,类型“左值参考A”用于代替A进行类型推导。

f,参数是模板参数的右值引用(T)。但随着 f2T<A> 不是模板参数。因此,该函数简单地将rvalue引用作为其参数 T<A>。调用无法编译,因为所有参数都是左值,并且这种情况没有使用左值调用的特殊异常。


2
2017-08-29 09:42



你能解释为什么会这样吗?为什么第二个不是通用参考? - Johannes Schaub - litb
@ JohannesSchaub-litb因为第二个不是对cv-unqualifed模板参数的右值引用 - 它是一个rvalue引用 T<A>。 T<A> 不是模板参数。 - Barry
我知道,但为什么T <A> &&不是一个普遍的参考? - Johannes Schaub - litb
@ JohannesSchaub-litb我不明白你在找什么。这就是为什么... - Barry


至于克服这个问题,我想一个或多或少等价的方法是用前向引用来推断它,并触发与 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>());
}

0
2017-08-29 10:45





有类型扣除是不够的。类型声明的形式必须是 究竟 T&& (左右参考只是一个 模板参数)。如果不是(或没有类型推导),则参数是右值引用。如果参数是左值,则不会编译。以来 T<A>&& 没有那种形式, f (T<A> && t) 无法接受左值(作为左值引用)并且您收到错误。如果你认为这需要太多的普遍性,那就考虑一下这个问题 const 限定符也打破了它:

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

(抛开const rvalue引用的相对无用)

除非最一般的形式,否则参考折叠的规则根本不起作用 T&& 用来。没有能力 f 认识到 左值 参数被传递并将参数视为 左值参考,没有参考崩溃要做(即崩溃 T& && 至 T& 不可能发生,而且只是 T<something>&&,一个右值参考。模仿类型)。函数确定rvalue或左值是否作为参数传递所需的机制在推导出的模板参数中进行编码。但是,此编码仅针对通用引用参数进行,严格定义。

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

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

需要有一种方法可以将参数显式定义为rvalue引用以排除lvalue参数。  这种推理似乎适用于其他类型的参数,包括cv资格。

此外,似乎有办法解决这个问题(见特征和SFINAE),但我无法回答这一部分。 :)


0
2017-08-29 15:57