Аргумент функции, возвращающий тип void или non-void - PullRequest
19 голосов
/ 22 мая 2019

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

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 -возвратных функций элегантным способом?

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

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

Ответы [ 4 ]

14 голосов
/ 22 мая 2019

, если вы можете поместить «некоторые общие вещи» в деструктор класса 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, совершенно законно.

9 голосов
/ 22 мая 2019

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

Протестировано с помощью 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;
}
5 голосов
/ 22 мая 2019

На мой взгляд, лучший способ сделать это - изменить способ вызова функций, которые могут возвращать пустоты.По сути, мы изменяем те, которые возвращают 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, что не так уж и дорого.

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

0 голосов
/ 22 мая 2019

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

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

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

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

Далее a 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> );

 }
...