我多年后回到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结束或者中断正常程序流(任何事件处理程序)之前保证,字符串对象被销毁?
答案 0 :(得分:4)
这实际上取决于String
是什么,但最有可能的是,log.c_str()
的返回值仅在log
本身有效时才有效({{{}} {1}})。这意味着在你的情况下,它根本不可用:当lambda返回时,std::string
被销毁,所以从lambda返回的指针已经悬空了。
幸运的是,解决方案很简单。将lambda的返回类型更改为log
,并让它自己返回String
。如果您最终需要log
,则可以在返回值上调用const char*
,您可以更好地控制生命周期。
答案 1 :(得分:1)
是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;
});
如果编译器不支持命名返回值优化,请将其转换为单行,以减少复制操作的数量。
答案 2 :(得分:1)
String
是lambda的本地(假设它或多或少像std::string
)你不能使用c_str
作为返回值,因为当调用者访问它时,本地已经是死。
另一个潜在的问题是,您通过[&]
变量pinSCL
和pinDOUT
来参考。如果lambda被存储起来并且它的生命周期在这两个变量的生命周期之后结束,那么调用它也是未定义的行为。
答案 3 :(得分:1)
欢迎回到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 *。所有这些对象比裸指针更安全,因为它们给你更少的机会来破坏对象的生命周期,并且是异常安全的(即,如果代码中间发生异常,你不会忘记自由你分配的记忆。)