92

I just watched Stephan T. Lavavej talk at CppCon 2018 on "Class Template Argument Deduction", where at some point he incidentally says:

In C++ type information almost never flows backwards ... I had to say "almost" because there's one or two cases, possibly more but very few.

Despite trying to figure out which cases he might be referring to, I couldn't come up with anything. Hence the question:

In which cases the C++17 standard mandates that type information propagate backwards?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Massimiliano
  • 7,842
  • 2
  • 47
  • 62

3 Answers3

80

Here is at least one case:

struct foo {
  template<class T>
  operator T() const {
    std::cout << sizeof(T) << "\n";
    return {};
  }
};

if you do foo f; int x = f; double y = f;, type information will flow "backwards" to figure out what T is in operator T.

You can use this in a more advanced way:

template<class T>
struct tag_t {using type=T;};

template<class F>
struct deduce_return_t {
  F f;
  template<class T>
  operator T()&&{ return std::forward<F>(f)(tag_t<T>{}); }
};
template<class F>
deduce_return_t(F&&)->deduce_return_t<F>;

template<class...Args>
auto construct_from( Args&&... args ) {
  return deduce_return_t{ [&](auto ret){
    using R=typename decltype(ret)::type;
    return R{ std::forward<Args>(args)... };
  }};
}

so now I can do

std::vector<int> v = construct_from( 1, 2, 3 );

and it works.

Of course, why not just do {1,2,3}? Well, {1,2,3} isn't an expression.

std::vector<std::vector<int>> v;
v.emplace_back( construct_from(1,2,3) );

which, admittedly, require a bit more wizardry: Live example. (I have to make the deduce return do a SFINAE check of F, then make the F be SFINAE friendly, and I have to block std::initializer_list in deduce_return_t operator T.)

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Very interesting answer, and I learned a new trick so thank you very much! I had to add a template deduction guideline to [make your example compile](http://coliru.stacked-crooked.com/a/90cd3d45b76a77ae), but other than that it works like a charm! – Massimiliano Nov 12 '18 at 22:28
  • 5
    The `&&` qualifier on the `operator T()` is a great touch; it helps avoid the poor interaction with `auto` by causing a compilation error if `auto` is misused here. – Justin Nov 12 '18 at 22:42
  • 1
    That's very impressive, could you point me to some reference/talk to the idea in the example? or maybe it's original :) ... – llllllllll Nov 12 '18 at 23:59
  • @tootsie No, it is an aggregate, and I used `{}`. I may need a deduction guide but I'm uncertain. Please note you could do this in 4 times the code and be twice as clear; I was being terse in my adding of a new language feature as a header file `#include` here. – Yakk - Adam Nevraumont Nov 13 '18 at 00:10
  • 3
    @lili Which idea? I count 5: Using operator T to deduce return types? Using tags to pass the deduced type to a lambda? Using conversion operators to roll-your-own placement object construction? Connecting all 4? – Yakk - Adam Nevraumont Nov 13 '18 at 00:13
  • @Yakk I meant the "more advanced way" example in your answer. If it's from some talk/reference/project, I'd like to read more. – llllllllll Nov 13 '18 at 00:22
  • I see what you mean..but my gcc only accepts it as an aggregate when I pass the type explicitly, something like.. `auto l = ; return deduce_return_t{move(l)};` – Tootsie Nov 13 '18 at 00:30
  • @toot c++17 and deduction guide should fix it too – Yakk - Adam Nevraumont Nov 13 '18 at 02:04
  • 1
    @lili Tha "more advanced way" example is, as I said, just 4 or so ideas glued together. I did the gluing on the fly for this post, but I certainly have seen many pairs or even triplets of those used together. It is a bunch of reasonably obscure techniques (as tootsie complains), but nothing novel. – Yakk - Adam Nevraumont Nov 13 '18 at 02:26
  • @Justin, could you please elaborate on how does the `&&` qualifier help produce an error in case `auto` is used? Actually, `auto` [does compile and run for me](https://wandbox.org/permlink/3pUhkARKKS02vhzR). Am I missing anything here? – Mike Nov 17 '18 at 11:09
  • @Mike It's not an easy situation to explain, but note that in your example, you are misusing this tool. In production code, the `f` member would be private. The `&&` qualifier means that someone can't accidentally write `auto result = construct_from(1, 2, 3)` and expect `result` to hold the value, whereas without the `&&` qualifier, if someone used `result`, it might just compile since the conversion could happen late. This can be a problem since the evaluation could happen multiple times if `result` is used multiple times, or it could actually cause bugs related to lifetime – Justin Nov 19 '18 at 19:12
  • @Justin, Nice explanation, thanks. A definitely clever use case for the `&&` qualifier. – Mike Nov 19 '18 at 19:23
31

Stephan T. Lavavej explained the case he was talking about in a tweet:

The case I was thinking of is where you can take the address of an overloaded/templated function and if it’s being used to initialize a variable of a specific type, that will disambiguate which one you want. (There’s a list of what disambiguates.)

we can see examples of this from cppreference page on Address of overloaded function, I have excepted a few below:

int f(int) { return 1; } 
int f(double) { return 2; }   

void g( int(&f1)(int), int(*f2)(double) ) {}

int main(){
    g(f, f); // selects int f(int) for the 1st argument
             // and int f(double) for the second

     auto foo = []() -> int (*)(int) {
        return f; // selects int f(int)
    }; 

    auto p = static_cast<int(*)(int)>(f); // selects int f(int)
}

Michael Park adds:

It's not limited to initializing a concrete type, either. It could also infer just from the number of arguments

and provides this live example:

void overload(int, int) {}
void overload(int, int, int) {}

template <typename T1, typename T2,
          typename A1, typename A2>
void f(void (*)(T1, T2), A1&&, A2&&) {}

template <typename T1, typename T2, typename T3,
          typename A1, typename A2, typename A3>
void f(void (*)(T1, T2, T3), A1&&, A2&&, A3&&) {}

int main () {
  f(&overload, 1, 2);
}

which I elaborate a little more here.

Shafik Yaghmour
  • 154,301
  • 39
  • 440
  • 740
  • 4
    We could also describe this as: cases where the type of an expression depends on the context? – M.M Nov 12 '18 at 23:56
20

I believe in static casting of overloaded functions the flow goes the opposite direction as in usual overload resolution. So one of those is backwards, I guess.

jbapple
  • 3,297
  • 1
  • 24
  • 38
  • 7
    I believe this is correct. And it is when you pass a function name to a function pointer type; type information flows from the context of the expression (the type you are assigning to/constructing/etc) backwards into the name of the function to determine which overload is chosen. – Yakk - Adam Nevraumont Nov 12 '18 at 22:08