什么时候类型信息在 C + + 中向后流动?

我刚刚看了斯蒂芬 · T · 拉瓦维奇在 CppCon 2018上关于“课堂模板论证演绎”的演讲,他在 总有一天上偶然说道:

在 C + + 中,类型信息几乎从不向后流动... ... 我不得不说“几乎”,因为有一两个案例,可能更多,但很少

尽管我试图找出他指的是哪些案子,我还是什么都想不出来。因此问题来了:

在哪些情况下,C + + 17标准要求类型信息向后传播?

5257 次浏览

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.

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.)

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.