Я нахожусь в процессе написания общего кода для будущей библиотеки. Я столкнулся со следующей проблемой внутри функции шаблона. Рассмотрим код ниже:
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*/);
}
После вашего редактирования: как это может работать с типами возврата void
? Либо вы присваиваете возвращаемое значение result
, либо оставляете его. auto return = some_void_func()
не имеет никакого смысла.
@MarekR, что если объект функции с rvalue передает только перегрузку operator()
? Будет ли работать без forward
?
Хорошо, честное слово, возможно, кто-то может определить такого оператора. Это довольно необычно, но возможно, поэтому std::forward
здесь может иметь смысл.
связанное предложение: open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0146r1.html
если вы можете поместить «некоторые общие вещи» в деструктор класса 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
, вполне допустима.
Бонусный совет: «общий материал» не должен ничего выбрасывать
А если что-то выкинет? Что делать, если вам нужно распространить это исключение?
@Barry - да: это еще один предел этого решения.
Какая-то специализация где-то необходима. Но цель здесь состоит в том, чтобы избежать специализации самой функции. Однако вы можете специализировать вспомогательный класс.
Протестировано с помощью 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 - но у другого ответа есть большой предел: если для «некоторых общих вещей» требуется значение, вычисленное с помощью f()
(если нет void
), я не вижу способа избежать где-либо специализации. Я подозреваю, что ответ Сэма лучше моего.
Это влечет за собой безусловную дополнительную копию.
Можно избежать дополнительной копии, если foo()
вернет сам вспомогательный объект. Что потребует смены абонентов.
@SamVarshavchik Это ... не решение.
На мой взгляд, лучший способ сделать это — изменить способ вызова функций, которые могут возвращать пустоту. По сути, мы меняем те, которые возвращают 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 - Да, это решение требует большого количества шаблонов, но позволяет использовать вычисленное значение в «общих материалах»; мое другое решение тривиально простое, но результат непригоден; принимая это во внимание, я думаю, что шаблон оправдан. Вы правы насчет конструкции и назначения по умолчанию; я думал добавить оговорку в ответ, но есть простой способ (немного больше шаблонного), чтобы избежать этой проблемы; ответ изменен. В любом случае, я нахожу ваше решение интересным, но действительно другим, потому что измените подпись функции (foo()
больше никогда не возвращайте void
).
... и теперь для этого требуется дополнительная копия и полное выделение!
@Barry - да, но избегайте проблемы построения и назначения по умолчанию; дополнительная копия связана с указателем.
Дополнительная копия — это не указатель, это полный объект. Это ингибирует РВО.
@Barry, извините, на какую дополнительную копию вы ссылаетесь?
Возвращаемый объект, return *pResult;
Это копия.
@Barry - «Также вы не можете «да, но» дополнительное выделение ...» - почему бы и нет?
Нет, в исходном коде не было лишней копии. Применяется копирование.
Вы не можете просто добавить дополнительное выделение/освобождение, потому что это упрощает решение головоломки метапрограммирования. Это огромный удар по производительности, который совершенно не нужен.
@Barry, скопируй элизион... да, ты прав. Но разве в вашем решении нет дополнительной копии (в случае void
/Void
)?
Все это потому, что якобы «действительно по-другому» вернуть void
вместо Void
? На самом деле он ничем не отличается ни концептуально, ни практически. Тип модуля - это тип модуля, во многих языках они есть более прямо - мы просто не имеем.
Нет, дополнительной копии нет. И Void
в любом случае пустой тип, так что даже если бы...?
Когда я говорю, что выделение совершенно не нужно, решение Сэма демонстрирует один из способов сделать это.
@Barry - я не согласен: ИМХО, вы не можете изменить сигнатуру функции, чтобы решить проблему такого типа.
@Barry - Хорошо: можно избежать выделения, и, вероятно, выделение тяжелое. Сказал так, при всех дефектах (ИМХО, самый некрасивый это повтор звонка f
) думаю, что этот способ может заинтересовать кого-то интересующегося этой проблемой.
Конечно, вы можете изменить подпись. Если вызывающая сторона передает вам функцию void, она все равно не ожидает возврата. Сравните это с преимуществом возможности писать нормальный код, И иметь возможность правильно обрабатывать исключения, И избегать лишних копий. Возвращение Void
далеко не недостаток, это само по себе преимущество, поскольку оно также позволяет этой функции компоноваться. Я использовал этот шаблон в качестве базового строительного блока нескольких библиотек, и ни разу я не использовал нужный в качестве возвращаемого типа, чтобы остаться void
.
Кстати, это также не будет работать, если TR_t
является ссылочным типом.
@Barry - "Кстати, это также не сработает, если TR_t
является ссылочным типом"; да, это большие ограничения, которых я не замечал; Благодарю. Ответ изменен, добавив ваши баллы (поправьте, если я что-то не так понял).
оффтоп: этот
std::forward
используется не по назначению. Его следует использовать только в том случае, если аргумент можно переместить в следующую функцию/шаблон. Здесь ничего не ломается, это просто шаблон.