我有两个函数系列,它们以两种不同的方式更新变量:
calculate
);update
)。在调用这些系列的范围时,我想统一与这些系列的工作(为了简单起见,我在这里省略了范围的使用,只传递一个值)。
一切都工作得很好,直到客户端代码中的开发人员错误地将参数“保留”在 lambda 中,误导了我的
process
函数。
查看代码:
#include <type_traits>
#include <iostream>
void process(float& value, auto fn) {
if constexpr (std::is_invocable_r_v<float, decltype(fn)>) {
std::cout << "Add a change" << std::endl;
value += fn();
}
else {
std::cout << "Update the value" << std::endl;
fn(value);
}
}
void update(float*v)
{
*v += 1.0f;
}
float calculate()
{
return 1.0f;
}
int main() {
float value = 0.0f;
// Process by adding a change
process(value, []() { return calculate(); });
// Process by updating the value
process(value, [](auto& v) { update(&v); });
// User mistakenly kept parameter 'auto&' and used lambda with calculate call,
// so the result will be lost in void process(...), since it will apply "Update the value" branch
process(value, [](auto&) { return calculate(); });
}
作为最后一个流程调用的结果,使用 lambda 调用
process
,该 lambda 接受一个参数,因此导致 fn(value)
调用并丢失 calculate
结果。
问题是,我是否可以在
process
中进行某种检查,以确保如果某些可调用对象作为 fn
传递,采用 float*
类型的参数,则它不能返回值(返回类型应该是void)这样就不会丢失?
任何类型的编译时错误或断言都可以。
我似乎应该以某种方式玩这个
std::is_invocable_r_v<float, decltype(fn)
,包括参数的类型,但我不知道怎么做。
当然,我可以编写一个包装器,将所有
calculate
函数带到 update
函数,我想这更容易、更安全,但是,如果没有这样的包装器,这可以完成吗?
@Igor Tandetnik 已经给出了一个适用于所提供示例的示例。您可以将
process()
函数修改为以下内容:
void process(float& value, auto fn) {
if constexpr (std::is_invocable_r_v<float, decltype(fn)>) {
std::cout << "Add a change" << std::endl;
value += fn();
}
else if constexpr (std::is_same_v<void, decltype(fn(value))>) {
std::cout << "Update the value" << std::endl;
fn(value);
}
else {
static_assert(false);
}
}
这是一个非常快速且简单的解决方案。但是,如果您想将时间浪费在(几乎)无用的替代方案上,那么您可以:
如果
fn
看起来像:
[](float v) { update(&v); })
更新分支将按预期执行,但它不会更新传递给它的值,而只是更新参数的本地副本。
如果您想更好地控制更新中要执行的内容以及添加分支中的内容,从而减少错误,您可以使用以下内容(我从here获得了帮助:
#include <concepts>
#include <type_traits>
#include <tuple>
namespace FT {
template <typename T>
struct FunctionTraits;
template <typename R, typename... Params>
struct FunctionTraits<R(*)(Params...)> {
using Ret = R;
using Arity = std::integral_constant<std::size_t, sizeof...(Params)>;
template <std::size_t i>
struct Args {
using type = std::tuple_element_t<i, std::tuple<Params...>>;
};
};
template <typename C, typename R, typename... Params>
struct FunctionTraits<R(C::*)(Params...) const> {
using Ret = R;
using Arity = std::integral_constant<std::size_t, sizeof...(Params)>;
template <std::size_t i>
struct Args {
using type = std::tuple_element_t<i, std::tuple<Params...>>;
};
};
template <typename T>
struct FunctionTraits : FunctionTraits<decltype(&T::operator())> {};
template <typename T>
requires std::is_function_v<T>
struct FunctionTraits<T> : FunctionTraits<decltype(&std::declval<T>())> {};
template <typename T, typename F>
concept Returns = std::is_same_v<T, typename FunctionTraits<F>::Ret>;
template <typename F>
constexpr std::size_t Arity = typename FunctionTraits<F>::Arity();
template <std::size_t i, typename F>
using ArgTypeAt = typename FunctionTraits<F>::template Args<i>::type;
}
template <typename F>
concept IsAdder = FT::Returns<float, F> && FT::Arity<F> == 0;
template <typename F>
concept IsUpdater = FT::Returns<void, F> && FT::Arity<F> == 1 && std::is_same_v<float&, FT::ArgTypeAt<0, F>>;
void process(float& value, auto fn) {
if constexpr (IsAdder<decltype(fn)>) {
std::cout << "Add a change" << std::endl;
value += fn();
}
else if constexpr (IsUpdater<decltype(fn)>) {
std::cout << "Update the value" << std::endl;
fn(value);
}
else {
static_assert(false);
}
}
这也强制(这不是我最初的意图)您不要使用
auto
作为传递给 lambda 的参数。相反,您必须直接使用 float
。