Коли інформація про тип рухається назад у 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?
Якк - Адам Неврамонт

1
Приклад @lili Tha "більш просунутий спосіб" - це, як я вже сказав, лише близько 4-х ідей, склеєних. Я робив склеювання на льоту для цього допису, але я, звичайно, бачив багато пар або навіть триплетів тих, що використовувались разом. Це купа досить незрозумілих прийомів (як скаржиться Тотсі), але нічого нового.
Якк - Адам Неврамонт,

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
Ми могли б також описати це як: випадки, коли тип виразу залежить від контексту?
ММ

20

Я вірю в статичну передачу перевантажених функцій, потік йде в протилежному напрямку, як у звичайному дозволі перевантаження. Тож одна з них - це, здається, назад.


7
Я вважаю, що це правильно. І це коли ви передаєте ім’я функції типу вказівника на функцію; інформація про тип перетікає з контексту виразу (типу, якому ви призначаєте / конструювання / і т.д.) назад до імені функції, щоб визначити, яке перевантаження обрано.
Якк - Адам Неврамонт,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.