Question

Summary: I want to end up with a function that deduces the exact types it was called with and takes (e.g.) a tuple that forwards them (the types of which will be different from the exact types the function was called with).

I'm stuck trying to "know" via deduction the types of the arguments to a given function, whilst simultaneously forwarding them. I think I might be missing something crucial about how this works.

#include <tuple>
#include <string>
#include <functional>

template <typename ...Args>
struct unresolved_linker_to_print_the_type {
   unresolved_linker_to_print_the_type();
};

void f(int,double,void*,std::string&,const char*) {
}

template <typename F, typename ...Args>
void g1(F func, Args&&... args) {
  unresolved_linker_to_print_the_type<Args...>();
  auto tuple = std::forward_as_tuple(args...);
  unresolved_linker_to_print_the_type<decltype(tuple)>();
}

template <typename F, typename T, typename ...Args>
void g2(F func, const T& tuple, Args... args) {
  unresolved_linker_to_print_the_type<Args...>();
  unresolved_linker_to_print_the_type<decltype(tuple)>();
}

int main() {
  int i;
  double d;
  void *ptr;
  std::string str;
  std::string& sref = str;
  const char *cstr = "HI";

  g1(f, i,d,ptr,sref,cstr);
  g2(f, std::forward_as_tuple(i,d,ptr,sref,cstr),  i,d,ptr,sref,cstr);
}

What I'd like to see is a scenario where when my function (e.g. g1 or g2) gets called it knows and can use both the original types - int,double,void*,std::string&,const char* and the forwarded arugments too.

In this instance I don't seem to be able to find this information from within g1 or g2. The (deliberate, to print out the types) linker error shows me in g1 they are:

int&, double&, void*&, std::string&, char const*&
int&, double&, void*&, std::string&, char const*&

and in g2:

int, double, void*, std::string, char const*
int&, double&, void*&, std::string&, char const*&

There are two thing I don't get here:

  1. Why do none of the printed (via the linker error) types match what I actually passed in? (int,double,void*,std::string&,const char). Can I deduce what I actually was passed? Preferably with "natural" syntax, i.e. everything just once and nothing explicitly written out. I can explicitly write:

    g2<decltype(&f),decltype(std::forward_as_tuple(i,d,ptr,sref,cstr)),int,double,void*,std::string&,const char*>(f,std::forward_as_tuple(i,d,ptr,sref,cstr),i,d,ptr,sref,cstr);
    

    but that's "unwieldy" to say the least!

  2. In g1 the presence of && in the function signature declaration seems to alter the types in the template parameter Args itself. Compare that with:

    template <typename T>
    void test(T t);
    

    Or:

    template <typename T>
    void test(T& t);
    

    using either of those with:

    int i;
    test(i);
    

    doesn't change the type of T. Why does the && change the type of T itself when & doesn't?

Was it helpful?

Solution

Answer to first question:

Arguments to functions are expressions, not types. The difference between these two is expressed in chapter 5 [expr], p5:

If an expression initially has the type “reference to T” (8.3.2, 8.5.3), the type is adjusted to T prior to any further analysis.

Thus, there is no difference what-so-ever between g(str) and g(sref). g() always sees a std::string, and never a reference.

Additionally expressions can be lvalue or rvalue (actually that's a simplification of the C++11 rules, but it is close enough for this discussion - if you want the details they're in 3.10 [basic.lval]).

Answer to second question:

Template parameters of the form:

template <class T>
void g(T&&);

are special. They are unlike T, T&, or even const T&& in the following way:

When T&& binds to an lvalue, T is deduced as an lvalue reference type, otherwise T deduces exactly as per the normal deduction rules.

Examples:

int i = 0;
g(i);  // calls g<int&>(i)
g(0);  // calls g<int>(0)

This behavior is to support so called perfect forwarding which typically looks like:

struct A{};

void bar(const A&);
void bar(A&&);

template <class T>
void foo(T&& t)
{
     bar(static_cast<T&&>(t));  // real code would use std::forward<T> here
}

If one calls foo(A()) (an rvalue A), T deduces per normal rules as A. Inside of foo we cast t to an A&& (an rvalue) and call bar. The overload of bar that takes an rvalue A is then chosen. I.e. if we call foo with an rvalue, then foo calls bar with an rvalue.

But if we call foo(a) (an lvalue A), then T deduces as A&. Now the cast looks like:

static_cast<A& &&>(t);

which under the reference collapsing rules simplifies to:

static_cast<A&>(t);

I.e. the lvalue t is cast to an lvalue (a no-op cast), and thus the bar overload taking an lvalue is called. I.e. if we call foo with an lvalue, then foo calls bar with an lvalue. And that's where the term perfect forwarding comes from.

OTHER TIPS

types (even in C++) are mostly a compile type notion (except of course the RTTI in vtables).

If you need entirely dynamic types, then C++ might not be the best language for that.

You might perhaps extend GCC (actually g++, assuming it is at least 4.6) with a plugin or a GCC MELT extension (MELT is a high level domain specific language to extend GCC) which does what you want (e.g. for instance providing an additional builtin which encode the type of its arguments in some constant string, etc...), but that does require some work (and is specific to GCC).

But I don't understand why you want to do such baroque things in C. If dynamic typing is so important to you, why don't you use a dynamically typed language??

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top