Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

No clang warning for returning a reference to function argument

Tags:

c++

c++11

clang

I understand that returning a reference to a function argument could invoke undefined behaviour as in the below example. The first 'MyType' created goes out of scope after the function call and is destroyed, leading to a dangling reference.

#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;
}

My questions are:

  • Why isn't there a clang warning for this? There is a warning for returning a ref to a local variable.
  • No matter how hard I tried allocating other things on the stack before the print, I couldn't get it to print the wrong thing without making the destructor explicitly change the data. Why is this?
  • Are there any valid (safe) use cases of returning a reference to an argument?
like image 467
silverscania Avatar asked Mar 26 '26 20:03

silverscania


1 Answers

No matter how hard I tried allocating other things on the stack before the print, I couldn't get it to print the wrong thing without making the destructor explicitly change the data. Why is this?

You're invoking undefined behaviour; anything could happen at this point!

Why isn't there a clang warning for this? There is a warning for returning a ref to a local variable.

Are there any valid (safe) use cases of returning a reference to an argument?

std::max

Suppose you want the max of two values:

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

You might not want a copy to be returned (for performance or semantic reasons). Luckily, std::max returns a reference, allowing for both use cases:

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

An example of where it matters for semantics reasons is when used in conjunction with std::swap:

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

Imagine that std::max returned a copy of y. Rather than swapping with x with y, x would we swapped with a copy of y. In other words, y would remain unchanged.

Assignment

A common use case of this is the assignment operator. Take this for example:

#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;
}

You can think of the hidden this parameter as a non-null pointer (so close enough to a reference for the purposes of this question).

Defining a copy-assignment operator as such is generally considered idomatic. One of the reasons this is so is because the automatically generated copy-assignment operator has that signature:

N4618 12.8.2.2 Copy/move assignment operator [class.copy.assign]

The implicitly-declared copy assignment operator for a class will have the form X& X::operator=(const X&)

Making this a warning would penalize canonical code! As for why one might want to do such a thing (aside from it being canonical), that's another topic...

like image 103
OMGtechy Avatar answered Mar 29 '26 10:03

OMGtechy