5

Consider the following code:

#include <iostream>
#include <tuple>
#include <utility>

// A.
template <typename... Args>
void f (const char* msg, Args&&... args)
{
    std::cout << "A. " << msg << "\n";
}

// B.
template <typename... Args>
void f (const char* msg, std::tuple<Args...>&& t)
{
    std::cout << "B. " << msg << "\n";
}

struct boo
{
    const std::tuple<int, int, long> g () const
    {
        return std::make_tuple(2, 4, 12345);
    }
};

int main ()
{
    f("First", 2, 5, 12345);
    f("Second", std::make_tuple(2, 5, 12345));

    boo the_boo;
    f("Third", the_boo.g());
    f("Fourth", std::forward<decltype(std::declval<boo>().g())>(the_boo.g()));

    return 0;
}

The output of that will be:

A. First
B. Second
A. Third
A. Fourth

From the output it's evident that it does not do what I would like it to do, that is I would like Third and Fourth to go through the B. version of the function. The std::forward from the Fourth call is superfluous as perfect forwarding does not happen there. In order to have perfect forwarding I know:

  • I must have an rvalue reference in a type deducing context
  • the type of the parameter must be a template type for the function

I understand it does not work. But I do not fully grasp:

  • why the context is changed by using std::tuple in such a way that it fails to work as desired ? Why the template parameter cannot be the type for another templated type?

  • how can I(elegantly) fix it ?

celavek
  • 5,575
  • 6
  • 41
  • 69
  • The bigger problem with `B` is that your non-const rvalue reference can't bind to a const rvalue. – T.C. Feb 11 '15 at 09:50
  • I see that. The compiler tells me the same thing(with a bit of code modification). I just do not see how to fix it. – celavek Feb 11 '15 at 09:53
  • 3
    Not having `g` return a const tuple? (Why does it return one, anyway?) – T.C. Feb 11 '15 at 09:58
  • @T.C. read previous to C++11 "Effective C++" series. – celavek Feb 12 '15 at 09:12
  • 3
    @celavek, don't return const values, it's a bad habit that dates from another decade and prevents move semantics. I'm not sure about Meyers but Sutter no longer recommends returning const values. It was always of questionable benefit anyway. – Jonathan Wakely Feb 13 '15 at 09:43
  • @Jonathan Wakely as I was saying in the comment to the answer I just learned that. Thanks for validating it once more. – celavek Feb 13 '15 at 10:14

1 Answers1

9

Your issue is that in Third and Fourth you are passing a const std::tuple where B. expects a non-const version.

When the compiler attempts to generate code for the call to f, it sees that you are calling with a const std::tuple and so deduces the type of Args... to be const std::tuple. Calling B. is not valid because the variable has a different const-qualification than expected.

To solve this, just make g() return a non-const tuple.


Edit:

In order for perfect forwarding to occur, you need a deduced context, as you say in your question. When you say std::tuple<Args...>&& in the function argument list, Args... is deduced, but std::tuple<Args...>&& is not; it can only by an rvalue reference. In order to fix this, that argument needs to be of the form T&& where T is deduced.

We can accomplish this using a custom type trait:

template <typename T>
struct is_tuple : std::false_type {};

template <typename... Args>
struct is_tuple <std::tuple<Args...>> : std::true_type {};

Then we use this trait to enable a single-argument template for tuples only:

// B.
template <typename T, typename = typename std::enable_if<
                          is_tuple<typename std::decay<T>::type>::value
                          >::type>
void f (const char* msg, T&& t)
{
    std::cout << "B. " << msg << "\n";
    std::cout << "B. is lval == " << std::is_lvalue_reference<T>() << "\n";
}

Or alternatively:

//! Tests if T is a specialization of Template
template <typename T, template <typename...> class Template>
struct is_specialization_of : std::false_type {};

template <template <typename...> class Template, typename... Args>
struct is_specialization_of<Template<Args...>, Template> : std::true_type {};

template <typename T>
using is_tuple = is_specialization_of<T, std::tuple>;

is_specialization_of taken from here and suggested by this question.

Now we have perfect forwarding!

int main ()
{
    f("First", 2, 5, 12345);
    f("Second", std::make_tuple(2, 5, 12345));

    boo the_boo;
    f("Third", the_boo.g());
    f("Fourth", std::forward<decltype(std::declval<boo>().g())>(the_boo.g()));

    auto the_g = the_boo.g();
    f("Fifth", the_g);

    return 0;
}

Outputs:

A. First
B. Second
B. is lval == 0
B. Third
B. is lval == 0
B. Fourth
B. is lval == 0
B. Fifth
B. is lval == 1
Community
  • 1
  • 1
TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • The answer has its merits as it made me realize and read more about the fact that in C++11 it's better not to return by const value(I was in the habit of doing that as per Effective C++ advice) BUT there is a slight nuance there: B. cannot accept a "const std::tuple" because it cannot move from it as move semantics are involved given its signature and not perfect forwarding. So my questions remain unanswered. I can do what you suggest(and in fact I tried) but that does not solve my issue in the context of perfect forwarding. – celavek Feb 12 '15 at 09:10
  • I see what you mean. What are the actual semantics of that function? It would probably be easier to have a separate one for accepting tuples. – TartanLlama Feb 12 '15 at 12:17
  • What do you mean by actual semantics? The real code case I have is exactly that - except the functions do something rather than printing something to stdout. You mean a function with a different name? I would still have to detect the fact that I'm receiving a tuple somewhere. – celavek Feb 12 '15 at 12:37
  • 1
    Why didn't I think of that? :) . I took the liberty to augment the answer with an alternative solution. Thank you – celavek Feb 13 '15 at 09:22
  • Nice, you could even declare `is_tuple` in terms of `is_specialization_of` as in my edit. – TartanLlama Feb 13 '15 at 09:28
  • 2
    `typename = typename std::enable_if::value>` is wrong for numerous of reasons: 1. you don't use a nested name specifier so it won't result in a substitution failure, 2. `T` can be deduced as an lvalue reference, so your trait fails to test it. forwarding based on the decltype of a function makes little sense as well – Piotr Skotnicki Feb 13 '15 at 09:55
  • @Piotr S. You are right of course, I just realized that; e.g. if I do * f("First Bis", 12345); * it will call *B.*. I corrected the answer. The last part of your comment I do not get though(declytype) - would you care to elaborate? – celavek Feb 13 '15 at 10:07
  • @celavek forwarding is for passing over arguments, what is the purpose of applying forward for a function call, where its result can be a prvalue itself ? – Piotr Skotnicki Feb 13 '15 at 10:50
  • @PiotrS. thanks for the feedback, do the latest edits fix the issues which you outlined? Are there any other problems with the solution you can see? – TartanLlama Feb 13 '15 at 10:55