Мне нужно преобразовать variant<type1, type2, ...>
в другой variant<ftype1, ftype2, ...>
, где ftypeN
— тип возвращаемого значения функции fn()
, вызываемой с параметром typeN
, при соблюдении следующих условий:
fn()
не вызывается на typeN
, тип ftypeN
равен wrong_arg_type
fn()
возвращает void
, тип ftypeN
— void_type
ftypeN
является возвращаемым типом fn(typeN)
Результирующий variant
должен иметь тот же индекс, что и входной variant
, и значение, равное fn(get<I>(v))
или инициализированное по умолчанию void_type
или wrong_arg_type
. Функцию fn()
следует вызывать не более одного раза.
Это должно работать с тремя текущими компиляторами C++23: gcc/clang/MSVC.
Это моя текущая реализация вывода типа возвращаемого значения:
struct void_type{};
struct wrong_arg_type{};
template<class Fn, class...Args>
using ret_t = std::conditional_t<std::is_invocable_v<Fn, Args...>,
std::conditional_t<std::is_void_v<std::invoke_result_t<Fn, Args...>>,
void_type,
std::invoke_result_t<Fn, Args...>>,
wrong_arg_type
>;
template<class Fn, class... Args>
constexpr auto invoke_fn(Fn fn, Args&&...args)
{
if constexpr(std::is_same_v<ret_t<Fn, Args...>, void_type>)
return std::invoke(std::forward<Fn>(fn), std::forward<Args>(args)...),
void_type{};
else if constexpr(std::is_same_v<ret_t<Fn, Args...>, wrong_arg_type>)
return wrong_arg_type{};
else
return std::invoke(fn, std::forward<Args>(args)...);
}
И моя текущая реализация функции transform
:
template<class Fn, class...T>
auto transform(Fn fn, const std::variant<T...>& v)
{
using variant_type = std::variant<ret_t<Fn, T>...>;
return [&]<auto N>(this auto&& self, std::size_t idx, std::index_sequence<N>)
-> variant_type
{
if constexpr(N == sizeof...(T))
{ throw "transform error"; }
else if (N == idx)
return variant_type(std::in_place_index<N>, invoke_fn(fn, std::get<N>(v)));
else
return self(idx, std::index_sequence<N + 1>{});
}(v.index(), std::index_sequence<0>{});
}
Демо-версия Compiler Explorer находится здесь.
В этом коде есть две проблемы, которые я сейчас не могу понять.
Во-первых, случай, когда функция не является вызываемой, не может скомпилироваться (std::tuple
в приведенной выше демонстрации).
<source>:15:29: error: no type named 'type' in 'struct std::invoke_result<overloaded<main()::<lambda(double)>, main()::<lambda(int)>, main()::<lambda(const std::string&)> >, std::tuple<int, int> >'
15 | std::conditional_t<std::is_void_v<std::invoke_result_t<Fn, Args...>>,
| ~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Во-вторых, MSVC выдает загадочную ошибку, даже если функция вызывается.
<source>(44): error C2231: '.fn': left operand points to 'class', use '->'
Не могли бы вы предложить решение этих проблем?
@Jarod42 - ты хочешь сказать, что conditional
тоже создает экземпляр ложного типа? какое тогда решение?
@Gene Общий способ ленивой оценки (короткого замыкания) с помощью std::conditional_t
Вы можете использовать немедленно вызываемую лямбду, чтобы определить тип возвращаемого значения:
template<class Fn, class...Args>
using ret_t = decltype([] {
if constexpr (std::is_invocable_v<Fn, Args...>) {
using R = std::invoke_result_t<Fn, Args...>;
if constexpr (std::is_void_v<R>)
return std::type_identity<void_type>{};
else
return std::type_identity<R>{};
} else
return std::type_identity<wrong_arg_type>{};
}())::type;
Я нашел решение проблемы std::conditional
, изменив реализацию invoke_fn
, поскольку оценивается только одна if constexpr
ветвь, а ret_t
является лишь decltype
ее.
template<class Fn, class... Args>
constexpr auto invoke_fn(Fn fn, Args&&...args)
{
if constexpr(not std::is_invocable_v<Fn, Args...>)
return wrong_arg_type{};
else if constexpr(std::is_void_v<std::invoke_result_t<Fn, Args...>>)
return std::invoke(std::forward<Fn>(fn), std::forward<Args>(args)...),
void_type{};
else
return std::invoke(fn, std::forward<Args>(args)...);
}
template<class Fn, class... Args>
using ret_t = decltype(invoke_fn(std::declval<Fn>(), std::declval<Args>()...));
Вторая проблема связана с ошибочной реализацией рекурсивной лямбды в MSVC. Замена ее функцией шаблона решает эту проблему.
template<auto N, class Fn, class...T>
auto transform_helper(Fn fn, const std::variant<T...>& v)
->std::variant<ret_t<Fn, T>...>
{
using variant_type = std::variant<ret_t<Fn, T>...>;
if constexpr(N == sizeof...(T))
{ throw "transform error"; }
else if (N == v.index())
return variant_type(std::in_place_index<N>, invoke_fn(fn, std::get<N>(v)));
else
return transform_helper<N+1>(fn, v);
}
template<class Fn, class...T>
auto transform(Fn fn, const std::variant<T...>& v)
{
return transform_helper<0>(fn, v);
}
Вот рабочая демо
Обычно я использую шаблоны ограничений C++20:
template<class Fn, class...Args>
struct ret_impl:
std::conditional
< std::is_void_v<std::invoke_result_t<Fn, Args...>>
, void_type
, std::invoke_result_t<Fn, Args...> > {};
template<class Fn, class...Args>
requires not std::is_invocable_v<Fn, Args...>
struct ret_impl:
std::type_identity<wrong_arg_type> {};
template<class Fn, class...Args>
using ret_t = typename ret_impl::type;
Это снимает необходимость иметь дело с несуществующими типами в случае, если ограничения не выполняются.
Более простое решение — decltype
. У вас уже есть invoke_fn
:
template<typename...T>
requires sizeof...(T) > 0
using ret_t = decltype(invoke_fn(std::declval<T>()...));
Далее я заметил, что вы увернулись std::visit
:
template<class Fn, class...T>
auto transform(Fn fn, const std::variant<T...>& v)
{
using variant = std::variant<ret_t<Fn, T>...>;
return std::visit
( [](auto&& a)
{ return variant {invoke_fn(std::forward<Fn>(fn), a)}; }
, v );
};
Это предполагаемый вариант использования std::variant
. Использование метапрограммирования в таких случаях не считается хорошей практикой.
на самом деле вы не можете использовать visit
, потому что он не сообщает вам индекс. Если у вас тот же тип, скажем, variant<int, int>
, visit
завершится ошибкой в variant
конструкторе, потому что это не уникальный тип.
Это патологический случай. Но вместо решения для конкретного случая следует использовать признак удаления дубликатов типов. Только представьте себе уровень сложности, связанный с двумя или более вариантами входных данных (auto tramsform(auto&& fn, auto&& ... vars);
). std::visit
может обрабатывать несколько входных аргументов одновременно. Таким образом, более коротким путем будет удаление дубликатов, если только значение индекса не должно иметь особое значение.
Red.Wave — в этом нет ничего патологического, и на самом деле это обычное дело, если подумать, как будет выглядеть вариант, хотя серьезные преобразования и функции будут возвращать одни и те же типы, особенно это касается void_type
и wrong_arg_type
. Ни в коем случае я не хочу иметь варианты проигрышных типов только потому, что они одинаковы.
другая возможность потерпеть неудачу — это когда типы различны, но конвертируемы (variant<int, long long int>
)
Если типы различаются путем явного указания типа, неявные преобразования можно отбросить: return my_variant{my_result{result}};
. Вы также можете настроить значение. Техника удаления дубликатов требует отдельного обсуждения.
При первой ошибке помните, что
std::conditional
не вызывает короткое замыкание, все типы должны быть действительными. с задержкой::type
может помочь.