CppCon 2014 学习:Return values take a ”closure” walk
这句话 ——
“Return values take a ‘closure’ walk. How to pass return values without specifying their type”
是在讨论 如何以更灵活的方式处理函数的返回值,尤其是在不明确指定类型的情况下。这在现代 C++(特别是 C++11 及以后)中与 lambda 表达式、泛型、auto
、decltype
、std::function
、std::any
等紧密相关。
理解要点:
“Return values take a closure walk” 是什么意思?
这是比喻性的说法,意思是:
“函数的返回值可以像闭包一样被延迟处理或传递,而不必显式说明其类型。”
比如在现代 C++ 中,我们可以用 auto
或 lambda 来捕获一个返回值、延迟执行或灵活转发:
auto result = []() {return 42; // 类型自动推导为 int
}();
或者:
std::function<void()> f = [x = computeValue()] {std::cout << x << "\n"; // computeValue 的返回类型可能很复杂,但无需写出来
};
如何不指定返回值类型?
1. 使用 auto
auto myFunction() {return someExpression;
}
编译器会根据 someExpression
自动推导返回值类型。
2. 使用 decltype(auto)
如果你想返回精确类型(包括引用、cv 修饰符):
decltype(auto) myFunction() {return someObj.someMethod();
}
3. 使用泛型 lambda / 函数模板
template<typename Func>
auto wrapper(Func f) {return f(); // 返回值类型由 f 决定
}
4. 使用 std::function
来传递返回值行为
std::function<int()> gen = []() { return 123; };
与“闭包”有关的用法示例
auto make_adder(int x) {return [=](int y) { return x + y; };
}
auto add5 = make_adder(5);
int result = add5(10); // 15
在这个例子中:
make_adder
返回一个闭包(lambda);- 不显式说明返回类型;
- 返回值可携带“状态”(即
x = 5
),这是闭包的核心特性。
总结
目标 | 技术 |
---|---|
不显式声明返回值类型 | auto , decltype(auto) |
返回/传递 lambda 表达式 | 闭包(closure) |
延迟返回、延迟执行 | lambda + std::function |
高度泛型、灵活接口 | 模板函数、概念、type-erased return (std::any ) |
简言之: | |
现代 C++ 鼓励你使用 lambda(闭包)与类型推导,让返回值“自由行走”于调用链中,而不是强行给它贴上标签。这让接口更灵活、表达更清晰、代码更少。 |
这个例子很好地展示了 “在某个上下文环境内调用函数” 的技巧,尤其是利用了 C++ 的 lambda(闭包)和 std::function
。下面帮你详细解释:
代码含义拆解
void callWithin(const std::function<void()>& fn)
{ ScopedContext context; // 创建一个上下文对象,构造时建立环境try { fn(); // 调用传入的函数(或闭包)} catch (SomeException& e) { // 这里可以捕获并处理异常}
}
ScopedContext context;
:假设这是一个 RAII 类型的对象,构造时设置一些“上下文环境”,析构时自动恢复或清理。fn()
是传入的函数/闭包,调用它时处于ScopedContext
环境中。- 捕获异常,避免异常泄漏或做特殊处理。
void printLine(const std::string& text)
{ std::cout << text << "\n";
}
这是一个简单函数,用来打印字符串。
callWithin([](){ printLine("Hello, CppCon"); });
调用 callWithin
,并传入一个 lambda,lambda 内部调用 printLine
。这样就确保 printLine
是在 ScopedContext
的上下文中执行的。
为什么这么写很有用?
- 封装上下文管理
例如设置线程本地变量、事务、锁、日志上下文,或者类似 COM 的CoInitialize
/CoUninitialize
。
这样可以避免重复写环境准备和清理代码。 - 异常安全
统一处理异常,防止崩溃,或者做日志、恢复等。 - 灵活性
你可以传入任意函数或闭包,且不需要声明参数或返回值,通用性强。
这里“理解”你说的:
- 函数参数是闭包或函数对象,通过
std::function<void()>
接收。 - ScopedContext 是上下文管理类,通过 RAII 实现。
- 调用函数时,自动带有上下文环境。
- 这是典型的**高阶函数(函数接收函数作为参数)**的用法。
- 在现代 C++ 里很常见,简化资源管理和代码结构。
你写的这个模板版本其实比之前的 std::function<void()>
版本更通用、更高效。
代码解析
template <typename Callable>
void callWithin(const Callable& fn)
{ScopedContext context; // 创建上下文,RAII 管理资源fn(); // 调用传入的可调用对象(函数、lambda、函数对象等)
}
- 这里用模板参数
Callable
,接受任意可调用类型(函数指针、函数对象、lambda)。 - 编译器会在调用时为不同的
Callable
生成对应的函数模板实例,避免了std::function
的类型擦除开销,通常性能更好。 - 使用模板还能避免
std::function
对参数类型的限制,比如传参和返回值都支持(只要调用表达式有效)。
为什么更好?
- 泛型更强:不局限于
void()
,可以传入带参数和返回值的函数(如果你调整callWithin
让它传参)。 - 零开销抽象:编译期内联,避免了运行时动态分发。
- 更灵活:任何符合调用操作符的类型都能用,甚至是状态ful 的函数对象。
举个例子
template <typename Callable>
void callWithin(const Callable& fn)
{ScopedContext context;fn();
}
void foo() { std::cout << "foo called\n"; }
callWithin(foo);
callWithin([]() { std::cout << "lambda called\n"; });
都能正常编译调用。
总结:
模板版本的 callWithin
是更现代、推荐的写法。
你这里用的是虚函数接口来实现“在某个上下文中调用函数”的模式。这样设计的好处和特点:
代码示意
class ContextManager
{
public:virtual void callWithin(const std::function<void()>& fn) = 0;
};
callWithin
是纯虚函数,表示任何继承ContextManager
的类都必须实现这个方法。- 接收一个
std::function<void()>
,意味着调用者可以传递任意无参无返回值的可调用对象(函数、lambda、函数对象)。
用途和设计意图
- 面向接口编程:通过虚函数抽象上下文调用,不同子类可以实现不同的上下文管理策略(比如事务管理、日志管理、异常捕获等)。
- 多态调用:你可以写代码只依赖
ContextManager
接口,运行时用不同的实现实例替换,增加灵活性。 - 函数回调封装:传入的
fn
函数体会在实现的上下文中执行,比如加锁、计时、异常处理。
举个例子
class ScopedContextManager : public ContextManager
{
public:void callWithin(const std::function<void()>& fn) override{ScopedContext context;fn();}
};
使用时:
ScopedContextManager mgr;
mgr.callWithin([](){ std::cout << "Inside context\n"; });
优缺点总结
- 优点:
- 通过虚函数实现运行时多态,灵活扩展不同上下文行为
- 统一接口调用方式
- 缺点:
- 使用
std::function
有额外开销(类型擦除、内存分配) - 虚函数调用有一定性能成本(间接调用)
- 模板版本通常更高效,但不支持运行时多态
如果你想实现跨多个上下文管理的灵活调用,这种虚接口模式是合适的。如果性能关键,且可接受编译时确定上下文,模板版本更优。
- 使用
你有个带返回值的函数 sum(double, double)
- 想通过
callWithin
这个上下文包装函数来调用,并且获取返回值
但是你给的例子:
double result = callWithin([](){ return sum(3.14, 2.71); });
这里的 callWithin
必须支持返回值,且能够传递这个返回值给外层。
下面是一个支持返回值的 callWithin
模板函数示例:
template<typename Callable>
auto callWithin(Callable&& fn) -> decltype(fn())
{ScopedContext context; // 设定上下文return fn(); // 调用函数,并返回结果
}
这样你就可以写:
double result = callWithin([](){ return sum(3.14, 2.71); });
说明
decltype(fn())
让callWithin
函数自动推导并返回fn()
的返回值类型- 这样既能包装上下文逻辑,也能支持不同返回值类型的函数调用
你这个版本的 callWithin
是专门针对返回 double
类型的函数,写成了:
double callWithin(const std::function<double()>& fn)
{ ScopedContext context; return fn();
}
特点:
- 参数是
std::function<double()>
,意味着传入的函数必须返回double
- 在调用函数前创建了一个
ScopedContext
(假设是某种上下文管理,比如自动资源管理) - 调用函数并返回它的
double
返回值
用法示例:
double sum(double a, double b) { return a + b; }
double result = callWithin([=]() { return sum(3.14, 2.71); });
这种写法限制了返回值必须是 double
,如果想支持任意返回类型,建议用模板版本。
如何写一个通用的 callWithin
函数,支持传入任意返回类型的函数,并正确返回结果。
关键点解析:
- 模板写法,返回类型自动推断:
template <typename Callable>
auto callWithin(const Callable& fn) -> decltype(fn())
{// 需要调用一个实现函数处理上下文callWithinImpl(fn); // 这里会报错,原因是类型不匹配// 还需要返回调用结果
}
decltype(fn())
自动推断fn
的返回类型,方便写泛型函数。- 这里调用了
callWithinImpl(fn)
,但callWithinImpl
只接受std::function<void()>
,传入类型不匹配导致编译失败。
- 辅助函数:
void callWithinImpl(const std::function<void()>& fn);
callWithinImpl
是已经定义好的,处理上下文和异常的函数,接受std::function<void()>
。
- 解决办法:用局部变量保存结果
template <typename Fn>
auto callWithin(const Fn& fn) -> decltype(fn())
{decltype(fn()) result{}; // 声明返回值变量// 在上下文内调用fn,保存返回值到result// ...return result;
}
- 这里声明了一个
result
变量来保存返回值。 - 具体调用和上下文管理的代码没写出来,需要补充。
总结:
- 你想写一个泛型函数
callWithin
,能接受任何有返回值的函数,并返回该返回值。 - 你想把实际调用和上下文管理的代码封装到
callWithinImpl
。 - 但
callWithinImpl
只能接受无返回值的std::function<void()>
,所以你需要一个中间层来:- 调用
fn
,拿到结果 - 传递无返回值的函数给
callWithinImpl
- 最后返回结果
- 调用
这个版本的 callWithin
利用了闭包捕获的机制,把返回值保存到外部变量里,然后传给只能接受 void()
函数的 callWithinImpl
,最后返回保存的结果。
代码结构讲解:
template <typename Fn>
auto callWithin(const Fn& fn) -> decltype(fn())
{decltype(fn()) result{}; // 先声明保存返回值的变量auto wrapperFn = [&]() -> void // 用一个无参、无返回值的lambda包裹{result = fn(); // 在闭包内调用fn,将结果保存到result};callWithinImpl(wrapperFn); // 调用处理上下文的实现函数,传入无返回值的lambdareturn result; // 返回保存的结果
}
关键点:
- 闭包捕获
[&]
:wrapperFn
捕获了外部的result
变量引用,所以可以修改它。 - 兼容接口:
callWithinImpl
只能接受std::function<void()>
类型的参数,使用无返回值lambda满足这个条件。 - 返回值保存:
fn()
的返回值被存入result
,这样在callWithinImpl
调用后还能得到正确返回值。
作用:
- 允许
callWithin
既支持带返回值的调用,又能利用已有只接受无返回值函数的上下文管理代码(callWithinImpl
)。 - 利用闭包巧妙地“绕过”类型限制,实现灵活复用。
这里结合了成员模板函数和虚函数接口,实现了灵活的调用上下文管理:
class ContextManager
{
public: template <typename Fn> auto callWithin(const Fn& fn) -> decltype(fn()) { decltype(fn()) result{}; callWithinImpl([&](){ result = fn(); }); return result; }
private: virtual void callWithinImpl(const std::function<void()>& fn) = 0;
};
// usage
double result = manager->callWithin([](){ return sum(3.14, 2.71); });
关键点解析:
callWithin
是模板函数,能接受任何可调用对象fn
,返回类型根据fn()
自动推断(decltype(fn())
)。result
用来存储fn()
的返回值。- 用一个捕获了
result
的无返回值 lambda 传给纯虚函数callWithinImpl
。 callWithinImpl
是纯虚函数,由派生类实现,接受std::function<void()>
,执行具体的上下文管理(如锁定、异常捕获等)。
优点:
- 通过模板,
callWithin
支持任意返回值类型,不用事先写特定类型版本。 - 通过虚函数,派生类可定制具体的上下文行为(比如日志、事务、异常处理等)。
- 保持接口简洁,调用方便。
使用示例:
class MyContextManager : public ContextManager {
private:void callWithinImpl(const std::function<void()>& fn) override {// 自定义上下文,例如:try {// 进入上下文fn();// 退出上下文} catch (...) {// 处理异常throw;}}
};
MyContextManager mgr;
double result = mgr.callWithin([](){ return 3.14 + 2.71; });
#include <functional>
#include <iostream>
class ContextManager {
public:template <typename Fn>auto callWithin(const Fn& fn) -> decltype(fn()) {decltype(fn()) result{};callWithinImpl([&]() { result = fn(); });return result;}
private:virtual void callWithinImpl(const std::function<void()>& fn) = 0;
};
class MyContextManager : public ContextManager {
private:void callWithinImpl(const std::function<void()>& fn) override {// 自定义上下文,例如:try {// 进入上下文fn();// 退出上下文} catch (...) {// 处理异常throw;}}
};
int main() {MyContextManager mgr;double result = mgr.callWithin([]() { return 3.14 + 2.71; });std::cout << result << std::endl;
}
这里的关键是:你传入的 lambda 是 void()
类型的函数,没有返回值。
callWithin
模板函数能兼容这种情况,因为:
- 如果传入的函数返回
void
,decltype(fn())
就是void
。 - 这时,你的
callWithin
需要做特殊处理,因为不能声明decltype(fn()) result{}
(即不能声明void
类型的变量)。
处理 void
返回类型的一种思路:
template <typename Fn>
auto callWithin(const Fn& fn) -> decltype(fn()) {if constexpr (std::is_void_v<decltype(fn())>) {// 函数返回void,不需要保存返回值callWithinImpl(fn);} else {// 函数有返回值,保存返回值decltype(fn()) result{};callWithinImpl([&]() { result = fn(); });return result;}
}
这样写:
- 如果
fn()
返回void
,直接调用callWithinImpl(fn)
,无返回值。 - 否则,按之前方式保存并返回结果。
例子:
void printLine(const std::string& text) {std::cout << text << "\n";
}
callWithin([]() { printLine("Hello, CppCon"); }); // 调用无返回值函数
这能正常编译并运行。
如果你需要,我可以帮你写一个完整版本的 ContextManager
类实现,包含对 void
返回值的处理!
你这里的问题是:
decltype(fn())
被推导为void
。- 所以你写的
decltype(fn()) result{}
实际是void result{}
,这是非法的。 - 然后你又试图给
result
赋值和返回,编译器报错。
这正是模板在处理返回类型为void
的函数时常见的坑。
解决思路:
针对返回类型为 void
和非 void
做不同处理:
template <typename Fn>
auto callWithin(const Fn& fn) -> decltype(fn())
{if constexpr (std::is_void_v<decltype(fn())>) {// 返回void,不声明变量,直接调用callWithinImpl(fn);// no return statement for void} else {// 非void,声明变量保存结果decltype(fn()) result{};callWithinImpl([&]() { result = fn(); });return result;}
}
if constexpr
是 C++17 新特性,编译时判断分支,只会编译对应路径,避免非法代码。- 如果你的编译器不支持 C++17,可以用重载或者
std::enable_if
实现类似效果。
C++11/14 版本重载写法示例:
// 处理void返回值
template <typename Fn>
auto callWithinImplVoid(const Fn& fn) -> void
{callWithinImpl(fn);
}
// 处理非void返回值
template <typename Fn>
auto callWithinImplNonVoid(const Fn& fn) -> decltype(fn())
{decltype(fn()) result{};callWithinImpl([&]() { result = fn(); });return result;
}
// 主接口
template <typename Fn>
auto callWithin(const Fn& fn) -> decltype(fn())
{return callWithinImplNonVoid(fn);
}
// 针对void返回重载
template <typename Fn>
void callWithin(const Fn& fn, typename std::enable_if<std::is_void<decltype(fn())>::value>::type* = 0)
{callWithinImplVoid(fn);
}
总结:
不能直接声明 void
类型变量,针对 void
返回类型需要特别处理。
C++17 以后推荐用 if constexpr
,更简洁。
C++11/14 可用模板重载或 SFINAE。
你这里用的是 类型萃取(type traits) 和 重载,实现了针对 void
和非 void
返回类型的不同处理:
template <typename Fn>
auto callWithin(const Fn& fn) -> decltype(fn()) {return _callWithin(fn, std::is_same<decltype(fn()), void>());
}
template <typename Fn>
void _callWithin(const Fn& fn, std::true_type) {callWithinImpl([&] { fn(); });
}
template <typename Fn>
auto _callWithin(const Fn& fn, std::false_type) -> decltype(fn()) {decltype(fn()) result{};callWithinImpl([&] { result = fn(); });return result;
}
- 主函数
callWithin
调用辅助函数_callWithin
,并通过std::is_same<decltype(fn()), void>()
传入类型标签(std::true_type
或std::false_type
)。 _callWithin
对void
返回的函数版本调用callWithinImpl
,直接执行,不返回结果。_callWithin
对非void
返回的函数版本,保存返回值result
,执行后返回。
这是经典的 标签分派(tag dispatching) 技巧,用来根据类型选择不同函数版本,实现了对void
返回类型的安全处理。
简言之:void
返回的函数调用后不返回值。- 非
void
返回的函数调用后返回结果。
callWithinImpl
函数尝试执行以下操作:
bool callWithinImpl(const std::function<void()>& fn) {try {auto dbConnectionScope = database->openConnection(); // this might failfn();} catch (DBException& e) {return false; // failure}return true; // ok
}
- 尝试打开数据库连接:
auto dbConnectionScope = database->openConnection();
这里打开数据库连接的操作可能会失败(例如抛出DBException
异常)。 - 调用传入的函数
fn()
:执行用户传入的代码块。 - 异常处理:如果在打开连接或执行函数过程中抛出了
DBException
,则捕获异常并返回false
,表示失败。 - 成功返回:没有异常时,返回
true
,表示成功。
这个函数负责在特定的上下文(数据库连接管理)中安全执行代码,并用返回值告知调用者执行是否成功。
template <typename Fn>
auto callWithin(const Fn& fn) -> boost::optional<decltype(fn())> {decltype(fn()) result{};bool ok = callWithinImpl([&]() { result = fn(); });if (ok)return result; // wrapped within boost::optionalelsereturn boost::none;
}
bool callWithinImpl(const std::function<void()>& fn);
这个模板函数 callWithin
是对之前 callWithinImpl
的封装,用来处理可能失败的执行并返回可选值(boost::optional
),具体流程如下:
- 模板参数
Fn
是传入的可调用对象,decltype(fn())
表示其返回值类型。 - 创建一个变量
result
用于存储调用fn()
的返回值。 - 调用
callWithinImpl
,并传入一个 lambda,该 lambda 调用fn()
并将结果赋给result
。 callWithinImpl
返回一个布尔值ok
表示执行是否成功。- 如果成功 (
ok == true
),则返回result
,自动包装成boost::optional<decltype(fn())>
。 - 如果失败 (
ok == false
),则返回boost::none
,表示没有有效结果。
这样设计可以优雅地捕获失败情况,并用boost::optional
明确告诉调用者是否有返回值。调用者可以通过判断optional
是否有值,来决定下一步处理。
这段代码是一个具体的示例,展示了如何使用 boost::optional
来处理函数调用可能失败的情况。以下是逐步解释:
1. 调用 callWithin
函数
auto result = callWithin([](){ return sum(1, 2); });
callWithin
是一个模板函数,它接受一个返回值的函数(这里是一个 lambda 函数[](){ return sum(1, 2); }
)。该 lambda 调用sum(1, 2)
来计算 1 和 2 的和(即3
)。callWithin
函数会执行这个 lambda 并将结果包装成一个boost::optional
类型。这样,结果就可以表示成功的返回值或者boost::none
,表示调用失败。
2. 检查 boost::optional
是否包含有效值
if (result) // might be boost::none
{double resultValue = *result; // dereference boost::optionalstd::cout << resultValue << std::endl;
}
if (result)
:这里使用boost::optional
的隐式转换来检查result
是否包含一个有效值。如果result
包含一个值(即boost::optional
不是boost::none
),那么if
条件成立,表示调用成功。*result
:如果result
有值,使用解引用操作符*
来提取存储的值(这里是sum(1, 2)
的返回值,即3
)。boost::optional
内部是一个包装类型,解引用操作符可以访问其中的实际值。std::cout << resultValue << std::endl;
:如果调用成功,打印结果resultValue
,在此例中是3
。
总结:
boost::optional
是用来表示可能存在或不存在的值的类型。如果callWithin
执行成功,result
会包含计算的结果;如果执行失败,result
会是boost::none
。- 通过
if (result)
来判断result
是否包含有效值,进而处理成功和失败的情况。 - 解引用:如果结果有效,可以通过
*result
获取实际的返回值,并进行处理。