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

时间:2016-12-17 23:23:56

标签: c++ c++11 clang

我理解返回对函数参数的引用可以调用未定义的行为,如下例所示。在函数调用之后创建的第一个“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;
}

我的问题是:

  • 为什么没有铿锵声警告?将ref返回到局部变量时会出现警告。
  • 无论我在打印之前尝试在堆栈上分配其他东西有多努力,我都无法在不使析构函数明确更改数据的情况下打印错误的东西。这是为什么?
  • 是否存在返回参数引用的有效(安全)用例?

2 个答案:

答案 0 :(得分:3)

  

无论我在打印之前如何努力在堆栈上分配其他东西,我都无法在不使析构函数明确更改数据的情况下打印错误的东西。这是为什么?

您正在调用未定义的行为;这一切都可能发生!

  

为什么没有铿锵声警告?将ref返回到局部变量时会出现警告。

     

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

<强>的std ::最大

假设您想要最多两个值:

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的副本。我们不会使用x yx进行交换,而是使用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参数视为非空指针(就此问题而言,它与参考的距离非常接近)。

通常认为定义副本赋值运算符是idomatic。其中一个原因是因为自动生成的复制赋值运算符具有该签名:

  

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

     

类的隐式声明的复制赋值运算符的格式为X& X::operator=(const X&)

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

答案 1 :(得分:1)

没有警告,因为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的潜在来源”并且它会检查每个调用。这不仅会导致额外的开销,而且有效地要求getref()inline(由于编译器通常被设计为一次仅在单个翻译单元上运行,因此必须定义getref()在每个模块中,它用于编译器保留此信息)。目前这是不可行的,但对于未来的编译器可能会变得更加实用(例如,如果跨模块优化成为标准)。
  • 假设程序员明智地永远不会传递getref() rvalue,或者特意打算让getref()成为潜在的UB来源,那么就不要发出警告。这是最可行的选择,因为它既不会损害合法代码,也不需要能够进行跨模块优化的编译器。

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