lambda 函数返回值的有效性
Posted
技术标签:
【中文标题】lambda 函数返回值的有效性【英文标题】:validity of the lambda function return value 【发布时间】:2018-05-16 07:59:05 【问题描述】:多年后我重新开始使用 C++ 编程,但我有一些疑问。
我创建了这个函数:
typedef std::function<const char *(void)> GetMessageLog;
void addLog(byte logLevel, GetMessageLog get)
if (loglevelActiveFor(LOG_TO_SERIAL, logLevel))
Serial.print(millis());
Serial.print(F(" : "));
Serial.println(get());
if (loglevelActiveFor(LOG_TO_SYSLOG, logLevel))
syslog(logLevel, get());
if (loglevelActiveFor(LOG_TO_WEBLOG, logLevel))
Logging.add(logLevel, get());
我想如下使用它:
addLog(LOG_LEVEL_INFO, [&]()
String log = F("HX711: GPIO: SCL=");
log += pinSCL;
log += F(" DOUT=");
log += pinDOUT;
return log.c_str();
);
保证log.c_str()的有效性直到addLog结束或者如果有什么东西打断了正常的程序流程(任何事件处理程序),字符串对象就被销毁了?
【问题讨论】:
【参考方案1】:这实际上取决于String
是什么,但最有可能的是,log.c_str()
的返回值仅在log
本身有效时才有效(std::string
肯定是这种情况)。这意味着在您的情况下,它根本不可用:当 lambda 返回时,log
被销毁,因此从 lambda 返回的指针已经悬空。
幸运的是,解决方案很简单。将 lambda 的返回类型更改为 String
并让它返回 log
本身。如果您最终需要const char*
,您可以在返回值上调用c_str()
,这样您可以更好地控制生命周期。
【讨论】:
【参考方案2】:String
是 lambda 的本地变量,因此(假设它或多或少类似于 std::string
)你不能使用 c_str
作为返回值,因为当调用者访问它时,本地已经死了。
另一个潜在的问题是,您使用[&]
引用变量pinSCL
和pinDOUT
来捕获。如果 lambda 被存储起来并且它的生命周期在这两个变量的生命周期之后结束,那么调用它也是未定义的行为。
【讨论】:
感谢您的回复。 pinSCL 和 pinDOUT 对于调用 addLog 的函数是本地的。 C++直到addLog的调用函数结束才保证有效性? @Wee 不,因为 lambda 是单独的范围。无论如何,它是一个闭包,本质上您使用operator()
创建了一个对象,其中包含成员变量-对那些捕获的变量的引用,lambda 表达式的主体就是该运算符。如果 addLog 实际上是异步调用 lambda 的,还必须小心,当本地函数退出并且 lambda 将无效时,这些引用将消失
@6502 这样定义addLog的目的是只有满足logLevel的条件才调用lambda方法。 addLog 在许多具有异构信息的方法中被调用。你会建议我如何实现它?
@Wee 有效性问题:a) lambda 对象构建需要时间。 b) 您的addLog
编写方式可能会调用 get() 三次 - 除非这些级别是独占的 - 如果是,是否有 else
被遗忘?检查 any 所需的日志记录级别是否处于活动状态并调用 get() 一次是否谨慎?
@Wee:不幸的是,C++ 没有垃圾收集器,这是完整实现 lambdas 通过引用捕获并超出创建它们的范围所必需的(这在文献中称为“向上 funarg 问题”) .您可以做的是 复制 lambda 函数中的值,而不是使用引用,但这并不总是可行的(您也可以尝试使用基于 shared_ptr
的更复杂的解决方案 - 但 C++ 再一次没有垃圾收集器,因此您最终可能会泄漏引用循环)。【参考方案3】:
欢迎回到 C++!你会发现它在本世纪发生了很大的变化,而且变得更好了。
对象“字符串日志”仅在调用 addLog 期间存在。您不能返回 log.c_str() ,因为这将返回一个指向在返回后将不再存在的对象的悬空指针。解决方案很简单——只返回“log”本身。让这个函数(和 GetMessageLog)返回不是旧式 C“char *”,而是现代 C++“std::string”。
在旧的 C++ 中,从函数中返回 std::string 常常令人不悦,因为它总是涉及该字符串的复制,有时甚至多次。随着移动构造函数的出现(这可能是 C++11 中最重要的新特性),这不再是真的。该函数构建一个字符串,当返回它时,该字符串不会被复制,而是“移动” - 这涉及仅复制它保存到其数据数组的指针,而不是复制数组本身。
在现代 C++ 中,您很少会像在本例中使用的 char* 那样使用旧式裸指针。你通常会使用像 std::string 这样的对象而不是 char*,像 std::vector 这样的容器而不是 int*,像 std::unique_ptr 这样的智能指针而不是 T*。所有这些对象都比裸指针更安全,因为它们让您更少机会弄乱对象的生命周期,并且是异常安全的(即,如果在代码中间发生异常,您不会忘记释放你分配的内存)。
【讨论】:
不,std::string
并非所有平台都支持(ISO 实际上不需要实现 all 类)。字符串是嵌入式平台的东西之一。如果那是那个平台,那么unique_ptr
根本不存在。
std::string 绝对是 C++11 标准的一部分。 std::unique_ptr 也是如此。如果您没有使用带有全套库的标准 C++,而是使用具有不同类的某种精简版本,那么您可能没有这些,但很可能您有它们的非标准变体。就像那个“String”,它大概类似于std::string,并且像它有一个移动构造函数一样,我上面所说的一切也适用于它。
这些是该平台的供应商标准,尽管实现不同(并且不太可能在 PC 平台上移植),但它在这方面的行为是相同的。没有移动构造函数..除非在新版本中进行了更改,否则移动在 Arduino 上不是那么有效。我能想到的最接近的是旧的 Delphi 类集,它们在接口上是相同的(对于进行格式化的构造函数)【参考方案4】:
那是 Arduino API 吗?这样会导致 UB,String 会在退出关闭时销毁其资源。
技术上,如果你重新设计类型
typedef std::function<String(void)> GetMessageLog;
那你就可以写了
addLog(LOG_LEVEL_INFO, [&]() -> String
String log = F("HX711: GPIO: SCL=");
log += pinSCL;
log += F(" DOUT=");
log += pinDOUT;
return log;
);
如果编译器不支持命名返回值优化,则将其转换为单行以减少复制操作的数量。
【讨论】:
感谢您的回复。这么定义的addLog的目的是只有满足logLdvel的条件才调用lambda方法。如果添加了重载,则不满足目的。 @Wee 我看到只有更改 GetMessageLog 的签名。不要尝试从字符串传递内部指针以上是关于lambda 函数返回值的有效性的主要内容,如果未能解决你的问题,请参考以下文章