Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
183 views
in Technique[技术] by (71.8m points)

c++ - 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.

In Scott Meyers' article referenced above (link), it is stated as a rule of thumb that:

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

Example 1

Indeed, using clang++ we see that the following code snippet will successfully compile with -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

Given any description of universal references (forward references) and type deduction (see, for instance, this explanation) it is clear why the above works. Although, from the same explanation, it is not abundantly clear why the below fails to work as well.

(failed) Example 2

This question addresses the same issue. The provided answers do not, however, explain why templated types are not categorized as being "deduced".

What I am about to show (seemingly) satisfies the requirement stated above by Meyers. However, the following code snipped fails to compile, producing the error (among others for each call to 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

In context, since the type T<A> of f's parameter is deduced, surely the parameter declaration T<A>&& t would behave as a universal reference (forward reference).

Example 3 (for clarity in describing the problem at hand)

Let me stress the following: the failure of the code in Example 2 to compile is not due to the fact that struct foo<> is a templated type. The failure seems to be cause only by the declaration of f's parameter as a templated type.

Consider the following revision to the previous code, which now does compile:

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

It is astonishing to me that this simple change completely alters the behaviour of the type deduction for the template function f's type parameter.

Questions:

Why does the second example not work as expected? Are there techniques to overcome this problem with templated types in c++11/14? Are there well known, extant codebases (in the wild) making successful use of c++'s forward references with templated types?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

When you call some function f with some lvalue:

int a = 42;
f(a);

Then f must be able to accept such an lvalue. This is the case when the first parameter of f is a (lvalue) reference type, or when it's not a reference at all:

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

This won't work when the parameter is a rvalue reference:

auto f(int &&); // error

Now, when you define a function with a forwarding reference as first parameter as you did in the first and third example ...

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

... and you actually call this function with an lvalue, template type deduction turns T into an (lvalue) reference (that this happens can be seen in the example code I provide in a moment):

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

Surely, there are too much references involved above. So C++ has collapsing rules, which are actually quite simple:

  • T& & becomes T&
  • T& && becomes T&
  • T&& & becomes T&
  • T&& && becomes T&&

Thanks to the second rule, the "effective" type of the first parameter of f is a lvalue reference, so you can bind your lvalue to it.

Now when you define a function g like ...

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

Then no matter what, template parameter deduction must turn the T into a template, not a type. After all, you specified exactly that when declaring the template parameter as template<class> class instead of typename. (This is an important difference, foo in your example is not a type, it's a template ... which you can see as type level function, but back to the topic)

Now, T is some kind of template. You cannot have a reference to a template. A reference (type) is built from a (possibly incomplete) type. So no matter what, T<A> (which is a type, but not a template parameter which could be deduced) won't turn into an (lvalue) reference, which means T<A> && doesn't need any collapsing and stays what it is: An rvalue reference. And of course, you cannot bind an lvalue to an rvalue reference.

But if you pass it an rvalue, then even g will work.

All of the above can be seen in the following example:

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)

Concerning how one could "overcome" this: You cannot. At least not the way you seem to want it to, because the natural solution is the third example you provide: Since you don't know the type passed (is it an lvalue reference, a rvalue reference or a reference at all?) you must keep it as generic as T. You could of course provide overloads, but that would somehow defeat the purpose of having perfect forwarding, I guess.


Hm, turns out you actually can overcome this, using some traits 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>;
};

You can then extract both the template and the type the template was instantiated with inside of the function:

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>&& isn't an universal reference because T<A> isn't a template parameter. It's (after deduction of both T and A) a simple (fixed / non generic) type.

A serious pitfall of making this a forwarding reference would be that you could no longer express the current meaning of T<A>&&: An rvalue reference to some type built from the template T with parameter A.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...