Когда информация о типе возвращается в C++?

Я только что видел, как Стефан Т. Лававей выступил на CppCon 2018 аргумента шаблона класса", где в какой-то момент он случайно сказал:

В C++ информация о типах почти никогда не течет в обратном направлении... Я должен был сказать "почти", потому что есть один или два случая, возможно, больше, но очень мало.

Несмотря на попытки выяснить, на какие случаи он может ссылаться, я ничего не смог придумать. Отсюда вопрос:

В каких случаях стандарт C++ 17 предписывает, чтобы информация о типах распространялась в обратном направлении?

Ответы

Ответ 1

Вот как минимум один случай:

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

если вы делаете foo f; int x = f; double y = f; foo f; int x = f; double y = f; , информация о типе будет течь "назад", чтобы выяснить, что T в operator T

Вы можете использовать это более продвинутым способом:

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

так что теперь я могу сделать

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

и это работает.

Конечно, почему бы просто не сделать {1,2,3}? Ну, {1,2,3} это не выражение.

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

которые, по общему признанию, требуют немного больше волшебства: живой пример. (Я должен сделать так, чтобы возвращаемый результат выводил проверку SFINAE на F, затем F был бы дружественным по отношению к SFINAE, и я должен заблокировать std :: initializer_list в операторе deduce_return_t T.)

Ответ 2

Стефан Т. Лававей объяснил случай, о котором он говорил, в твиттере:

Случай, о котором я думал, - это где вы можете взять адрес перегруженной/шаблонной функции и, если она используется для инициализации переменной определенного типа, это будет однозначно определять, какую вы хотите. (Есть список того, что неоднозначно.)

мы можем увидеть примеры этого на странице cppreference в Address of overloaded function (некоторые из них приведены ниже):

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

Майкл Парк добавляет:

Это также не ограничивается инициализацией конкретного типа. Это может также сделать вывод только из числа аргументов

и предоставляет этот живой пример:

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

который я уточню немного больше здесь.

Ответ 3

Я верю в статическое приведение перегруженных функций, поток идет в противоположном направлении, как при обычном разрешении перегрузки. Так что, я думаю, один из них задом наперед.