Как преобразовать std::variant, применив указанную функцию

Мне нужно преобразовать variant<type1, type2, ...> в другой variant<ftype1, ftype2, ...>, где ftypeN — тип возвращаемого значения функции fn(), вызываемой с параметром typeN, при соблюдении следующих условий:

  • если функция fn() не вызывается на typeN, тип ftypeN равен wrong_arg_type
  • если функция fn() возвращает void, тип ftypeNvoid_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 '->'

Не могли бы вы предложить решение этих проблем?

При первой ошибке помните, что std::conditional не вызывает короткое замыкание, все типы должны быть действительными. с задержкой ::type может помочь.

Jarod42 07.07.2024 03:18

@Jarod42 - ты хочешь сказать, что conditional тоже создает экземпляр ложного типа? какое тогда решение?

Gene 07.07.2024 03:28

@Gene Общий способ ленивой оценки (короткого замыкания) с помощью std::conditional_t ​​

Remy Lebeau 07.07.2024 03:45
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
112
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы можете использовать немедленно вызываемую лямбду, чтобы определить тип возвращаемого значения:

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 конструкторе, потому что это не уникальный тип.

Gene 07.07.2024 19:28

Это патологический случай. Но вместо решения для конкретного случая следует использовать признак удаления дубликатов типов. Только представьте себе уровень сложности, связанный с двумя или более вариантами входных данных (auto tramsform(auto&& fn, auto&& ... vars);). std::visit может обрабатывать несколько входных аргументов одновременно. Таким образом, более коротким путем будет удаление дубликатов, если только значение индекса не должно иметь особое значение.

Red.Wave 07.07.2024 20:08

Red.Wave — в этом нет ничего патологического, и на самом деле это обычное дело, если подумать, как будет выглядеть вариант, хотя серьезные преобразования и функции будут возвращать одни и те же типы, особенно это касается void_type и wrong_arg_type. Ни в коем случае я не хочу иметь варианты проигрышных типов только потому, что они одинаковы.

Gene 07.07.2024 20:17

другая возможность потерпеть неудачу — это когда типы различны, но конвертируемы (variant<int, long long int>)

Gene 07.07.2024 20:26

Если типы различаются путем явного указания типа, неявные преобразования можно отбросить: return my_variant{my_result{result}};. Вы также можете настроить значение. Техника удаления дубликатов требует отдельного обсуждения.

Red.Wave 07.07.2024 20:42

Другие вопросы по теме