Аргумент функции, возвращающий тип void или non-void

Я нахожусь в процессе написания общего кода для будущей библиотеки. Я столкнулся со следующей проблемой внутри функции шаблона. Рассмотрим код ниже:

template<class F>
auto foo(F &&f) {
    auto result = std::forward<F>(f)(/*some args*/);
    //do some generic stuff
    return result;
}

Он будет работать нормально, если я не передам ему функцию, которая возвращает void, например:

foo([](){});

Теперь, конечно, я мог бы использовать некоторую магию std::enable_if, чтобы проверить тип возвращаемого значения и выполнить специализацию для функции, возвращающей void, которая выглядит следующим образом:

template<class F, class = /*enable if stuff*/>
void foo(F &&f) {
    std::forward<F>(f)(/*some args*/);
    //do some generic stuff
}

Но это ужасно дублировало бы код фактически логически эквивалентных функций. Можно ли это сделать простым и элегантным способом как для void-возвращающих, так и для не-void-возвратных функций?

Обновлено: существует зависимость данных между функцией f() и общими вещами, которые я хочу сделать, поэтому я не принимаю во внимание такой код:

template<class F>
auto foo(F &&f) {
    //do some generic stuff
    return std::forward<F>(f)(/*some args*/);
}

оффтоп: этот std::forward используется не по назначению. Его следует использовать только в том случае, если аргумент можно переместить в следующую функцию/шаблон. Здесь ничего не ломается, это просто шаблон.

Marek R 22.05.2019 14:31

После вашего редактирования: как это может работать с типами возврата void? Либо вы присваиваете возвращаемое значение result, либо оставляете его. auto return = some_void_func() не имеет никакого смысла.

andreee 22.05.2019 14:31

@MarekR, что если объект функции с rvalue передает только перегрузку operator()? Будет ли работать без forward?

bartop 22.05.2019 14:35

Хорошо, честное слово, возможно, кто-то может определить такого оператора. Это довольно необычно, но возможно, поэтому std::forward здесь может иметь смысл.

Marek R 22.05.2019 14:44

связанное предложение: open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0146r1.html

geza 22.05.2019 15:19
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
18
5
1 766
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

если вы можете поместить «некоторые общие вещи» в деструктор класса bar (внутри блока try/catch безопасности, если вы не уверены, что это не вызывает исключений, как указал Дракс), вы можете просто написать

template <typename F>
auto foo (F &&f)
 {
   bar b;

   return std::forward<F>(f)(/*some args*/);
 }

Итак, компилятор вычисляет f(/*some args*/), выполняет деструктор b и возвращает вычисленное значение (или ничего).

Обратите внимание, что return func();, где func() — функция, возвращающая void, вполне допустима.

Бонусный совет: «общий материал» не должен ничего выбрасывать

Drax 22.05.2019 14:43

А если что-то выкинет? Что делать, если вам нужно распространить это исключение?

Barry 23.05.2019 14:20

@Barry - да: это еще один предел этого решения.

max66 23.05.2019 14:27

Какая-то специализация где-то необходима. Но цель здесь состоит в том, чтобы избежать специализации самой функции. Однако вы можете специализировать вспомогательный класс.

Протестировано с помощью gcc 9.1 с -std=c++17.

#include <type_traits>
#include <iostream>

template<typename T>
struct return_value {


    T val;

    template<typename F, typename ...Args>
    return_value(F &&f, Args && ...args)
        : val{f(std::forward<Args>(args)...)}
    {
    }

    T value() const
    {
        return val;
    }
};

template<>
struct return_value<void> {

    template<typename F, typename ...Args>
    return_value(F &&f, Args && ...args)
    {
        f(std::forward<Args>(args)...);
    }

    void value() const
    {
    }
};

template<class F>
auto foo(F &&f)
{
    return_value<decltype(std::declval<F &&>()(2, 4))> r{f, 2, 4};

    // Something

    return r.value();
}

int main()
{
    foo( [](int a, int b) { return; });

    std::cout << foo( [](int a, int b) { return a+b; }) << std::endl;
}
«Некоторая специализация где-то необходима». не с кодом Finally raii, как предлагается в другом ответе.
Jarod42 22.05.2019 15:03

@ Jarod42 - но у другого ответа есть большой предел: если для «некоторых общих вещей» требуется значение, вычисленное с помощью f() (если нет void), я не вижу способа избежать где-либо специализации. Я подозреваю, что ответ Сэма лучше моего.

max66 22.05.2019 15:13

Это влечет за собой безусловную дополнительную копию.

Barry 22.05.2019 16:20

Можно избежать дополнительной копии, если foo() вернет сам вспомогательный объект. Что потребует смены абонентов.

Sam Varshavchik 22.05.2019 16:29

@SamVarshavchik Это ... не решение.

Barry 22.05.2019 16:37

На мой взгляд, лучший способ сделать это — изменить способ вызова функций, которые могут возвращать пустоту. По сути, мы меняем те, которые возвращают void, чтобы они вместо этого возвращали какой-то тип класса Void, то есть, во всех смыслах и целях, одно и то же, и пользователям на самом деле все равно.

struct Void { };

Все, что нам нужно сделать, это обернуть вызов. В следующем примере используются имена C++17 (std::invoke и std::invoke_result_t), но все они легко реализуемы в C++14:

// normal case: R isn't void
template <typename F, typename... Args, 
    typename R = std::invoke_result_t<F, Args...>,
    std::enable_if_t<!std::is_void<R>::value, int> = 0>
R invoke_void(F&& f, Args&&... args) {
    return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}

// special case: R is void
template <typename F, typename... Args, 
    typename R = std::invoke_result_t<F, Args...>,
    std::enable_if_t<std::is_void<R>::value, int> = 0>
Void invoke_void(F&& f, Args&&... args) {
    // just call it, since it doesn't return anything
    std::invoke(std::forward<F>(f), std::forward<Args>(args)...);

    // and return Void
    return Void{};
}

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

template<class F>
auto foo(F &&f) {
    auto result = invoke_void(std::forward<F>(f), /*some args*/);
    //do some generic stuff
    return result;
}

И вам не нужно ни пихать всю свою логику в деструктор, ни дублировать всю свою логику, выполняя специализацию. За счет foo([]{}) возврата Void вместо void, что не так уж дорого.

И затем, если Обычная пустота когда-либо будет принят, все, что вам нужно сделать, это заменить invoke_void на std::invoke.

Если вам нужно использовать result (в непустых случаях) в «некоторых общих вещах», я предлагаю решение на основе if constexpr (поэтому, к сожалению, не раньше C++17).

Не очень элегантно, если честно.

Прежде всего, определите «истинный тип возвращаемого значения» f (с учетом аргументов)

using TR_t = std::invoke_result_t<F, As...>;

Затем переменная constexpr, чтобы увидеть, является ли возвращаемый тип void (просто чтобы немного упростить следующий код)

constexpr bool isVoidTR { std::is_same_v<TR_t, void> };

Теперь мы определяем (потенциально) «фальшивый тип возврата»: int, когда истинный тип возврата равен void, TR_t в противном случае

using FR_t = std::conditional_t<isVoidTR, int, TR_t>;

Затем мы определяем интеллектуальный указатель на значение результата как указатель на «фальшивый возвращаемый тип» (так что int в случае пустоты)

std::unique_ptr<FR_t>  pResult;

Проходя через указатель, вместо простой переменной типа «фальшивый возвращаемый тип», мы можем работать также, когда TR_t не является конструируемым по умолчанию или не может быть назначено (ограничения, указанные Барри (спасибо), первой версии этого ответа) .

Теперь, используя if constexpr, два случая для выполнения f (это, ИМХО, самая уродливая часть, потому что мы должны написать два раза один и тот же вызов f)

if constexpr ( isVoidTR )
   std::forward<F>(f)(std::forward<As>(args)...);
else
   pResult.reset(new TR_t{std::forward<F>(f)(std::forward<As>(args)...)});

После этого «некоторые общие вещи», которые могут использовать result (в непустых случаях), а также `isVoidTR).

В заключение еще один if constexpr

if constexpr ( isVoidTR )
   return;
else
   return *pResult;

Как указал Барри, это решение имеет некоторые важные недостатки, потому что (не void случаи)

  • требуют распределения
  • требуется дополнительная копия в переписке return
  • вообще не работает, если TR_t (тип, возвращаемый f()) является ссылочным типом

Во всяком случае, ниже приведен пример полной компиляции C++17.

#include <memory>
#include <type_traits>

template <typename F, typename ... As>
auto foo (F && f, As && ... args)
 {
   // true return type
   using TR_t = std::invoke_result_t<F, As...>;

   constexpr bool isVoidTR { std::is_same_v<TR_t, void> };

   // (possibly) fake return type
   using FR_t = std::conditional_t<isVoidTR, int, TR_t>;

   std::unique_ptr<FR_t>  pResult;

   if constexpr ( isVoidTR )
      std::forward<F>(f)(std::forward<As>(args)...);
   else
      pResult.reset(new TR_t{std::forward<F>(f)(std::forward<As>(args)...)});

   // some generic stuff (potentially depending from result,
   // in non-void cases)

   if constexpr ( isVoidTR )
      return;
   else
      return *pResult;
 }

int main ()
 {
   foo([](){});

   //auto a { foo([](){}) };  // compilation error: foo() is void

   auto b { foo([](auto a0, auto...){ return a0; }, 1, 2l, 3ll) };

   static_assert( std::is_same_v<decltype(b), int> );

 }

Это решение налагает конструкцию и назначение по умолчанию в качестве требований (в дополнение к большому количеству шаблонов). Основная идея замены void на обычный тип, тем не менее, хороша — см. мой ответ, чтобы узнать, как лучше реализовать эту идею.

Barry 23.05.2019 04:45

@Barry - Да, это решение требует большого количества шаблонов, но позволяет использовать вычисленное значение в «общих материалах»; мое другое решение тривиально простое, но результат непригоден; принимая это во внимание, я думаю, что шаблон оправдан. Вы правы насчет конструкции и назначения по умолчанию; я думал добавить оговорку в ответ, но есть простой способ (немного больше шаблонного), чтобы избежать этой проблемы; ответ изменен. В любом случае, я нахожу ваше решение интересным, но действительно другим, потому что измените подпись функции (foo() больше никогда не возвращайте void).

max66 23.05.2019 12:26

... и теперь для этого требуется дополнительная копия и полное выделение!

Barry 23.05.2019 14:08

@Barry - да, но избегайте проблемы построения и назначения по умолчанию; дополнительная копия связана с указателем.

max66 23.05.2019 14:12

Дополнительная копия — это не указатель, это полный объект. Это ингибирует РВО.

Barry 23.05.2019 14:16

@Barry, извините, на какую дополнительную копию вы ссылаетесь?

max66 23.05.2019 14:21

Возвращаемый объект, return *pResult; Это копия.

Barry 23.05.2019 14:22

@Barry - «Также вы не можете «да, но» дополнительное выделение ...» - почему бы и нет?

max66 23.05.2019 14:25

Нет, в исходном коде не было лишней копии. Применяется копирование.

Barry 23.05.2019 14:26

Вы не можете просто добавить дополнительное выделение/освобождение, потому что это упрощает решение головоломки метапрограммирования. Это огромный удар по производительности, который совершенно не нужен.

Barry 23.05.2019 14:29

@Barry, скопируй элизион... да, ты прав. Но разве в вашем решении нет дополнительной копии (в случае void/Void)?

max66 23.05.2019 14:30

Все это потому, что якобы «действительно по-другому» вернуть void вместо Void? На самом деле он ничем не отличается ни концептуально, ни практически. Тип модуля - это тип модуля, во многих языках они есть более прямо - мы просто не имеем.

Barry 23.05.2019 14:30

Нет, дополнительной копии нет. И Void в любом случае пустой тип, так что даже если бы...?

Barry 23.05.2019 14:31

Когда я говорю, что выделение совершенно не нужно, решение Сэма демонстрирует один из способов сделать это.

Barry 23.05.2019 14:32

@Barry - я не согласен: ИМХО, вы не можете изменить сигнатуру функции, чтобы решить проблему такого типа.

max66 23.05.2019 14:33

@Barry - Хорошо: можно избежать выделения, и, вероятно, выделение тяжелое. Сказал так, при всех дефектах (ИМХО, самый некрасивый это повтор звонка f) думаю, что этот способ может заинтересовать кого-то интересующегося этой проблемой.

max66 23.05.2019 14:36

Конечно, вы можете изменить подпись. Если вызывающая сторона передает вам функцию void, она все равно не ожидает возврата. Сравните это с преимуществом возможности писать нормальный код, И иметь возможность правильно обрабатывать исключения, И избегать лишних копий. Возвращение Void далеко не недостаток, это само по себе преимущество, поскольку оно также позволяет этой функции компоноваться. Я использовал этот шаблон в качестве базового строительного блока нескольких библиотек, и ни разу я не использовал нужный в качестве возвращаемого типа, чтобы остаться void.

Barry 23.05.2019 14:50

Кстати, это также не будет работать, если TR_t является ссылочным типом.

Barry 23.05.2019 15:01

@Barry - "Кстати, это также не сработает, если TR_t является ссылочным типом"; да, это большие ограничения, которых я не замечал; Благодарю. Ответ изменен, добавив ваши баллы (поправьте, если я что-то не так понял).

max66 23.05.2019 15:16

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