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

92

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

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

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

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

Массимилиано
источник
частичная специализация и деструктуризация сопоставления с образцом.
v.oddou

Ответы:

80

Вот хотя бы один случай:

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

если вы это сделаете 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.)

Якк - Адам Неврамонт
источник
Очень интересный ответ, и я узнал новый трюк, так что спасибо вам большое! Мне пришлось добавить руководство по выводу шаблона, чтобы ваш пример компилировался , но в остальном он работает как шарм!
Массимилиано
5
&&Отборочные на operator T()это большой сенсорный; это помогает избежать плохого взаимодействия с auto, вызывая ошибку компиляции, если autoздесь неправильно используется.
Джастин
1
Это очень впечатляет, не могли бы вы указать мне на какую-нибудь ссылку / поговорить об идее в примере? или , может быть , это оригинальный :) ...
LLLLLLLLLL
3
@lili Какая идея? Я считаю 5. Использование оператора T для определения возвращаемых типов? Использование тегов для передачи выведенного типа лямбде? Используете операторы преобразования для создания собственного объекта размещения? Подключение всех 4?
Yakk - Adam Nevraumont
1
@lili Tha пример "более продвинутого пути", как я уже сказал, состоит всего из 4 идей, склеенных вместе. Я делал склейку на лету для этого поста, но наверняка видел много пар или даже троек, которые использовались вместе. Это набор довольно непонятных приемов (как жалуется Тутси), но ничего нового.
Yakk - Adam Nevraumont
31

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

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

мы можем увидеть примеры этого на странице cppreference в Адресе перегруженной функции , я исключил несколько ниже:

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

о котором я подробнее расскажу здесь .

Шафик Ягмур
источник
4
Мы могли бы также описать это как: случаи, когда тип выражения зависит от контекста?
MM
20

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

jbapple
источник
7
Я считаю, что это правильно. И это когда вы передаете имя функции типу указателя функции; информация о типе перетекает из контекста выражения (тип, который вы назначаете / конструируете / и т. д.) обратно в имя функции, чтобы определить, какая перегрузка выбрана.
Yakk - Adam Nevraumont