返回对函数参数的引用时没有 clang 警告

No clang warning for returning a reference to function argument

本文关键字:clang 警告 引用 函数 参数 返回      更新时间:2023-10-16

我知道返回对函数参数的引用可能会调用未定义的行为,如以下示例所示。创建的第一个"MyType"在函数调用后超出范围并被销毁,导致引用悬空。

#include <iostream>
struct MyType {
std::string data;
inline ~MyType() {
data = "Destroyed!!";
}
};
const MyType& getref(const MyType& x) {
return x;
}
int main(int argc, char *argv[]) {
const MyType& test = getref(MyType {"test"});
std::cout << test.data << std::endl;
return 0;
}

我的问题是:

  • 为什么没有叮当警告?有一个警告,要求将引用返回到局部变量。
  • 无论我在打印之前多么努力地尝试在堆栈上分配其他内容,如果不使析构函数显式更改数据,我都无法让它打印错误的内容。这是为什么呢?
  • 是否有任何返回参数引用的有效(安全)用例?

无论我在打印之前多么努力地尝试在堆栈上分配其他内容,如果不使析构函数显式更改数据,我都无法让它打印错误的内容。这是为什么呢?

你正在调用未定义的行为;此时任何事情都可能发生!

为什么没有叮当警告?有一个警告,要求将引用返回到局部变量。

是否有任何返回参数引用的有效(安全)用例?

标准::最大

假设您想要两个值的最大值:

auto foo = std::max(x, y); // foo is a copy of the result

您可能不希望返回副本(出于性能或语义原因)。幸运的是,std::max返回一个引用,允许两种用例:

auto  foo = std::max(x, y); // foo is a copy of the result
auto& bar = std::max(x, y); // bar is a reference to the result

出于语义原因,它很重要的一个例子是与std::swap结合使用时:

std::swap(x, std::max(y, z));

想象一下,std::max返回了一份y.与其用y交换x,不如x交换y的副本。换言之,y将保持不变。

分配

一个常见的用例是赋值运算符。举个例子:

#include <iostream>
class T {
public:
T(int _x) : x(_x) { }
T& operator=(const T& rhs) { x = rhs.x; return *this; }
int getX() const { return x; }
private:
int x = 0;
};
int main() {
T instanceA(42);
T instanceB(180);
std::cout << (instanceA = instanceB).getX() << std::endl;
}

您可以将隐藏的this参数视为非空指针(对于此问题而言,与引用足够接近)。

定义复制赋值运算符本身通常被认为是偶像。之所以如此,原因之一是因为自动生成的复制赋值运算符具有该签名:

N4618 12.8.2.2 复制/移动赋值运算符 [class.copy.assign]

类的隐式声明复制赋值运算符将具有以下形式X& X::operator=(const X&)

将此作为警告会惩罚规范代码!至于为什么人们可能想做这样的事情(除了它是规范的),那是另一个话题......

没有警告,因为const T&很可能是对左值的引用,就像它是对右值的引用一样。

void func() {
MyType mt;
const MyType& l = getref(mt);
const MyType& r = getref(MyType{});
}

考虑到getref(mt)是完全有效的,这给我们留下了三个选择:

  • 对任何同时接受和返回(cv) T&的函数发出警告,无论它是用左值还是右值调用的。 这会惩罚法典,所以这不是一个好的选择。 [虽然仅在函数专门返回其参数时才发出警告会更好地实现这一点,但由于下面提到的原因,这是不可行的。 因此,如果参数和返回值是同一类型,编译器将发出错误。
    [请注意,这可能会导致完美转发出现问题。 例如,std::move()使用完美转发通过引用或右值引用获取参数,然后返回对该参数的右值引用。 任何不考虑这一点的算法都会将其视为获取和返回T&&
  • 在向getref()传递右值时发出警告。 这要求编译器始终将"如果传递右值,则getref()是UB的潜在来源"的信息保存在内存中,并且检查每个调用。 这不仅会导致额外的开销,而且实际上需要inlinegetref()(由于编译器通常设计为一次只对一个翻译单元进行操作,因此必须在编译器用于保留此信息的每个模块中定义getref())。 这目前是不可行的,但对于未来的编译器来说可能会变得更加实用(例如,如果跨模块优化成为标准)。
  • 永远不要发出警告,假设程序员足够聪明,永远不会传递getref()右值,或者特别打算让getref()成为 UB 的潜在来源。 这是最可行的选择,因为它既不会惩罚法律代码,也不需要能够跨模块优化的编译器。

因此,大多数编译器通常会选择不发出警告,假设程序员知道他们在做什么。 即使指定了-Wall,Clang、GCC、ICC 和 MSVC 也不会为此发出任何警告。