临时std::istringstream对象传入Lambda作为提取表达式的技术疑问
问题场景
我有一个解析命令行参数的函数,内部多个if分支需要执行相似的操作(给istringstream设置failbit异常),为了减少冗余尝试用lambda封装,但不确定当前实现是否有未定义行为,也不清楚正确的实现方式。
简化代码如下:
int foo(int argc, char* argv[]) { using ss = std::istringstream; auto sf = [&](ss&& stream) -> ss& { stream.exceptions(ss::failbit); return stream; }; int retVal = 0; int myInt, myOtherInt; // 补充原代码中缺失的变量声明 bool valA = false; bool valB = false; try { for(int i=1; i < argc; i++) { std::string arg( argv[i] ); if( !valA ) { valA = true; sf( ss(arg) ) >> myInt; } else if( !valB ) { valB = true; sf( ss(arg) ) >> std::hex >> myOtherInt; } } } catch( std::exception& err ) { retVal = -1; std::cerr << err.what() << std::endl; } return retVal; }
尝试将lambda参数声明为左值引用ss&时编译报错:cannot bind non-const lvalue reference of type 'ss&',改为右值引用ss&&后编译通过,但不确定用法是否正确,核心疑问如下:
- 临时对象
ss(arg)的生命周期能否覆盖整个提取表达式? - 若可以实现示例中的用法,应如何正确定义该lambda?
解答
1. 临时对象的生命周期覆盖问题
你的担心是多余的,临时对象ss(arg)的生命周期完全可以覆盖整个提取表达式。
根据C++标准,临时对象会在完整表达式结束后才被销毁。这里的完整表达式是指sf(ss(arg)) >> myInt(或对应的十六进制提取语句)——从创建临时流对象,到调用lambda配置异常,再到完成数据提取的整个语句。
另外,当临时对象绑定到lambda的右值引用参数stream时,该临时对象的生命周期会被延长至函数调用结束,但即便如此,整个提取操作是在同一个完整表达式内完成的,临时流对象在所有操作执行完毕前都不会被销毁,因此不会出现未定义行为。
2. lambda的正确实现方式
你的当前实现本身是合法的,但可以根据需求优化得更通用或更清晰:
方式一:保留右值引用参数(适配临时对象场景)
如果你的lambda只需要处理临时创建的istringstream对象,当前的写法完全没问题,它能正确绑定临时对象并返回引用供链式调用:
auto sf = [](ss&& stream) -> ss& { stream.exceptions(ss::failbit); return stream; };
注意这里捕获列表[&]是多余的,因为lambda内部没有访问外部变量,可以改成空捕获[]。
方式二:使用模板lambda(兼容左值/右值流对象,C++14及以上)
如果后续可能需要处理已存在的左值流对象,模板lambda可以同时兼容左值和右值:
auto sf = [](auto&& stream) -> decltype(auto) { stream.exceptions(decltype(stream)::failbit); return std::forward<decltype(stream)>(stream); };
decltype(auto)会自动推导返回值类型,std::forward则能保持原对象的值类别,这样不管传入左值还是右值流对象,都能正确处理。
方式三:直接返回临时对象(避免引用)
如果你不需要复用流对象,也可以让lambda直接创建并配置流,返回值而非引用,这样写法更简洁:
auto sf = [](const std::string& arg) -> ss { ss stream(arg); stream.exceptions(ss::failbit); return stream; }; // 调用方式:sf(arg) >> myInt;
这种方式无需担心引用生命周期问题,因为返回的是移动后的流对象(C++11及以上支持移动语义,istringstream是可移动的)。
内容的提问来源于stack exchange,提问作者LostSnail

