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

Аргумент функции, возвращающий тип 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 -returning, так и non- void -returning?

РЕДАКТИРОВАТЬ: есть зависимость данных между функцией f() и общими вещами, которые я хочу сделать, поэтому я не принимаю код, подобный этому:

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

Ответ 1

если вы можете поместить "некоторые общие вещи" в деструктор класса bar (внутри блока try/catch безопасности, если вы не уверены, что он не генерирует исключения, как указано Drax), вы можете просто написать

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

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

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

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

Ответ 2

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

Протестировано с 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;
}

Ответ 3

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

struct Void { };

Все, что нам нужно сделать, это обернуть вызов. Следующее использует имена С++ 17 (std::invoke и std::invoke_result_t), но все они могут быть реализованы в С++ 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, что не так уж и дорого.

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

Ответ 4

Вы можете использовать if constexpr здесь:

template<class F>
auto foo(F &&f) {
    //do some generic stuff
    if constexpr (!std::is_same<F, void>::value)
        return std::forward<F>(f)(/*some args*/);
}

Это должно автоматически определить тип возвращаемого значения вашего шаблона функции.

Работает с лязгом и g++.

После редактирования: Попытка присвоить возвращаемое значение возможной функции void какой-либо переменной невозможна. Вам нужно либо отдельно рассматривать случай void (например, специализация, SFINAE), либо вы должны пересмотреть свой вариант использования.

Ответ 5

В случае, если вам нужно использовать result (в не пустых случаях) в "некотором родовом материале", я предлагаю if constexpr основанное на if constexpr (так, к сожалению, не до С++ 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, два случая для exec f (это, IMHO, самая уродливая часть, потому что мы должны написать два раза один и тот же вызов 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 (тип, возвращаемый TR_t f()) является ссылочным типом

В любом случае, следующий пример полной компиляции С++ 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> );

 }