范围变量循环导致返回局部变量的地址引用?
ranged for loop of variables results in returning address-reference of local variable?
// g++ --std=c++17 test.cpp -I /usr/local/include -L /usr/local/lib -lboost_system -Wall -pedantic -Wreturn-type -Wstrict-aliasing -Wreturn-local-addr -fsanitize=address -g
// LD_LIBRARY_PATH=/usr/local/lib ./a.out
#include <iostream>
#include <boost/filesystem.hpp>
namespace fs = boost::filesystem;
class A {
public:
fs::path path_;
const fs::path & path() const { return path_; }
fs::path & path() { return path_; }
};
class B {
public:
fs::path root_path_;
A path_2;
A path_3;
const fs::path & operator()() const {
for ( const auto & path : {
path_3.path(),
path_2.path(),
root_path_
}) {
if ( not path.empty() ) {
return path;
}
}
throw std::logic_error{"using default-constructed B"};
}
};
int main(int argc, char **argv) {
B b;
b.root_path_ = "foo/";
b.path_2.path() = "foo/bar";
b.path_3.path() = "foo/baz";
std::cout << b() << '\n';
return 0;
}
据我所知,上面的代码似乎是有效的 C++。相反,在调用时,我得到垃圾输出。
g++
一开始并没有抱怨,但是 Address Sanitizer 会。 g++
最后在添加 -O2
时抱怨。生成的警告是
test.cpp: In member function ‘const boost::filesystem::path& B::operator()() const’:
test.cpp:31:12: warning: function may return address of local variable [-Wreturn-local-addr]
return path;
^~~~
test.cpp:29:3: note: declared here
}) {
^
注意我用的是:
$ cat /etc/fedora-release
Fedora release 25 (Twenty Five)
$ g++ --version
g++ (GCC) 6.3.1 20161221 (Red Hat 6.3.1-1)
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
请注意,我使用指针解决了 错误。
const fs::path & operator()() const {
for ( const auto * path : {
&path_3.path(),
&path_2.path(),
&root_path_
}) {
if ( not path->empty() ) {
return *path;
}
}
throw std::logic_error{"using default-constructed B"};
}
但是,这确实给我留下了一些疑问:
- 为什么在添加
-O2
之前g++
没有抱怨这个问题?
- 究竟我的代码是未定义的?我会说它的定义很明确:
B::operator() const
是……好吧……常量。那应该是说里面的对象used不是locals就是const成员。它访问 const 成员。它构造了一个局部变量 const auto &
,它...应该引用一个 const 成员字段。到底是什么导致它绑定到临时的?
编译器没有义务对未定义的行为发出诊断。如果编译器可以检测到语法上有效但导致未定义行为的代码,然后抱怨它,那只是锦上添花。 gcc 的 -O2
打开额外的优化,并进行额外的代码分析;因此,gcc 有时只能在启用优化的情况下检测未定义的行为。
您的范围迭代似乎超过了 临时 std::initializer_list
。范围迭代变量是对初始化列表的引用。因此,该函数最终返回对临时对象的引用,这就是 gcc 在这里咆哮的内容。由于临时对象在方法 returns 时被销毁,该方法最终返回对已销毁对象的引用。对该引用的任何使用都包含未定义的行为。
当您将临时范围转换为指针列表时,您是按值迭代,并且您不是返回对临时范围的引用,而是取消引用范围中的值,这是一个完美的 kosher 指针。
关注以下内容from here:
The underlying array is not guaranteed to exist after the lifetime of
the original initializer list object has ended. The storage for
std::initializer_list is unspecified (i.e. it could be automatic,
temporary, or static read-only memory, depending on the situation).
绑定到范围迭代的初始化列表在迭代结束时结束的生命周期。其中包括从方法返回。因此,初始化列表不再存在,您只是返回了一个悬空引用。
// g++ --std=c++17 test.cpp -I /usr/local/include -L /usr/local/lib -lboost_system -Wall -pedantic -Wreturn-type -Wstrict-aliasing -Wreturn-local-addr -fsanitize=address -g
// LD_LIBRARY_PATH=/usr/local/lib ./a.out
#include <iostream>
#include <boost/filesystem.hpp>
namespace fs = boost::filesystem;
class A {
public:
fs::path path_;
const fs::path & path() const { return path_; }
fs::path & path() { return path_; }
};
class B {
public:
fs::path root_path_;
A path_2;
A path_3;
const fs::path & operator()() const {
for ( const auto & path : {
path_3.path(),
path_2.path(),
root_path_
}) {
if ( not path.empty() ) {
return path;
}
}
throw std::logic_error{"using default-constructed B"};
}
};
int main(int argc, char **argv) {
B b;
b.root_path_ = "foo/";
b.path_2.path() = "foo/bar";
b.path_3.path() = "foo/baz";
std::cout << b() << '\n';
return 0;
}
据我所知,上面的代码似乎是有效的 C++。相反,在调用时,我得到垃圾输出。
g++
一开始并没有抱怨,但是 Address Sanitizer 会。 g++
最后在添加 -O2
时抱怨。生成的警告是
test.cpp: In member function ‘const boost::filesystem::path& B::operator()() const’:
test.cpp:31:12: warning: function may return address of local variable [-Wreturn-local-addr]
return path;
^~~~
test.cpp:29:3: note: declared here
}) {
^
注意我用的是:
$ cat /etc/fedora-release
Fedora release 25 (Twenty Five)
$ g++ --version
g++ (GCC) 6.3.1 20161221 (Red Hat 6.3.1-1)
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
请注意,我使用指针解决了 错误。
const fs::path & operator()() const {
for ( const auto * path : {
&path_3.path(),
&path_2.path(),
&root_path_
}) {
if ( not path->empty() ) {
return *path;
}
}
throw std::logic_error{"using default-constructed B"};
}
但是,这确实给我留下了一些疑问:
- 为什么在添加
-O2
之前g++
没有抱怨这个问题? - 究竟我的代码是未定义的?我会说它的定义很明确:
B::operator() const
是……好吧……常量。那应该是说里面的对象used不是locals就是const成员。它访问 const 成员。它构造了一个局部变量const auto &
,它...应该引用一个 const 成员字段。到底是什么导致它绑定到临时的?
编译器没有义务对未定义的行为发出诊断。如果编译器可以检测到语法上有效但导致未定义行为的代码,然后抱怨它,那只是锦上添花。 gcc 的
-O2
打开额外的优化,并进行额外的代码分析;因此,gcc 有时只能在启用优化的情况下检测未定义的行为。您的范围迭代似乎超过了 临时
std::initializer_list
。范围迭代变量是对初始化列表的引用。因此,该函数最终返回对临时对象的引用,这就是 gcc 在这里咆哮的内容。由于临时对象在方法 returns 时被销毁,该方法最终返回对已销毁对象的引用。对该引用的任何使用都包含未定义的行为。
当您将临时范围转换为指针列表时,您是按值迭代,并且您不是返回对临时范围的引用,而是取消引用范围中的值,这是一个完美的 kosher 指针。
关注以下内容from here:
The underlying array is not guaranteed to exist after the lifetime of the original initializer list object has ended. The storage for std::initializer_list is unspecified (i.e. it could be automatic, temporary, or static read-only memory, depending on the situation).
绑定到范围迭代的初始化列表在迭代结束时结束的生命周期。其中包括从方法返回。因此,初始化列表不再存在,您只是返回了一个悬空引用。