Это можно сделать, но это а) довольно сложно, и б) не очень хорошая идея, 99,9% времени. Вот как вы продолжаете. Единственный способ сделать что-либо, основываясь на типе, которому вы назначаете выражение, - это воспользоваться преимуществами неявных преобразований. Для этого требуется шаблонный неявный оператор диалога, который не может быть объявлен локально в лямбде, поэтому мы должны начать с написания небольшого количества кода поддержки:
template <class T>
struct identity {
T get(); // not defined
};
template <class F>
struct ReturnConverter {
F f;
template <class T>
operator T() {
return f(identity<T>{});
}
};
template <class F>
auto makeReturnConverter(F f) { return ReturnConverter<F>{f}; }
Первый класс - просто помочь лямбде с выводящими типами. Второй класс - тот, который сам принимает лямбду (или любой вызываемый) и имеет неявный оператор преобразования для любого типа. Когда запрашивается преобразование, оно вызывает вызываемый объект, используя наш шаблон класса identity
как способ подачи типа. Теперь мы можем использовать это так:
auto doIt = [] (long l) {
return makeReturnConverter([=] (auto t) {
return l + sizeof(decltype(t.get()));
});
};
Эта лямбда создает наш специальный класс ReturnConverter
, кормя другую лямбду. Эта лямбда захватывает аргумент long l
внешней лямбды (по значению), и она готова принять наш специальный класс идентичности в качестве единственного аргумента. Затем он может отказаться от «цели» или типа назначения. Я использую sizeof
здесь, чтобы быстро показать пример, где результат зависит как от аргумента лямбда-выражения, так и от целевого типа. Но учтите, что как только я получу тип, используя decltype(t.get())
, я смогу объявить переменную этого типа и сделать с ней все, что захочу.
float x = doIt(5);
double y = doIt(2);
После этих вызовов x
будет 9 (число с плавающей запятой 4, + 5) и y
будет 10 (double это размер 8, + 2).
Вот более интересный пример:
auto doIt2 = [] (long l) {
return makeReturnConverter([=] (auto t) {
decltype(t.get()) x;
for (std::size_t i = 0; i != l; ++i ) {
x.push_back(i*i);
}
return x;
});
};
std::vector<int> x = doIt2(5);
std::deque<int> y = doIt2(5);
Здесь я могу собрать стандартный контейнер в соответствии с тем, что запрашивает левая сторона, если в стандартном контейнере есть метод push_back
.
Как я уже сказал, для начала это чрезмерно сложно и трудно оправдать в подавляющем большинстве случаев. Обычно вы можете просто указать конкретный тип в качестве явного (не выводимого) параметра шаблона и auto
слева. Я использовал это в очень определенных случаях все же. Например, в gtest, когда вы объявляете тестовые фикстуры, любые повторно используемые данные объявляются как переменные-члены в фикстуре. Нестатические переменные-члены должны быть объявлены с их типом (не может использовать auto), поэтому я использовал этот трюк, чтобы позволить быстро создавать определенные типы данных фикстуры, сохраняя при этом повторение как можно ближе к нулю. В данном случае это нормально, потому что код, код которого не обязательно должен быть очень надежным, но действительно хочет свести к минимуму повторение практически любой ценой; обычно это не очень хороший компромисс (и с auto
, доступным слева, обычно не требуется).