prevent pass-by-ref of temporary object
Asked Answered
R

4

10

I have a class that 'remembers' a reference to some object (e.g. an integer variable). I can't have it reference a value that's destructed immediately, and I'm looking for a way to protect the users of my class from doing so by accident.

Is an rvalue-reference overload a good way to prevent a temporary to be passed in?

struct HasRef {
    int& a;
    HasRef(int& a):a(a){}
    void foo(){ a=1; }
};


int main(){
    int x=5;
    HasRef r1(x);
    r1.foo();  // works like intended.

    HasRef r2(x+4);
    r2.foo(); // dereferences the temporary created by x+4

 }

Would a private rvalue overload do?

 struct HasRef {
   int& a;
   HasRef( int& a ):a(a){}
   void foo(){ a=1; }
 private: 
   HasRef( int&& a );
 };

 ... HasRef r2(x+1); // doesn't compile => problem solved?

Are there any pitfalls I didn't see?

Reasoning answered 20/6, 2012 at 20:38 Comment(9)
A temporary doesn't bind to an lvalue reference. The definition of r2 in your first example should not compile.Moulmein
If you are using VC++, one solution is to turn up the warning level and it will tell you when it doesn't work.Yare
However, a const reference would bind to a temporary, so the question is still a very good one. I've considered this approach, but I'm still of the opinion that if a class is going to store a reference (or pointer) to the referenced object, it's better to take a pointer in the constructor, to make potential lifetime concerns a bit more obvious (when a constructor takes a pointer, usually it makes me think twice about what the object is going to do with it).Lymph
@musiphil,@Dave: indeed; I've mind-distilled this class from something from my day job which caused a crash in VS10, but ran smoothly in gcc4.4; will provide better code tomorrow.Reasoning
@JamesMcNellis: But if it's a const reference, then it extends the lifetime of the temporary, so it's no longer a problem.Carbamate
@BenjaminLindley: No, it extends the lifetime of the temporary only until construction completes. After the object is constructed, a refers to a no-longer-existent object.Lymph
Of course, I should have said "A temporary doesn't bind to a non-const lvalue reference." :-)Moulmein
@xtofl, can you find other questions for which the pass-by-rvalue-reference tag could apply? It seems exceptionally specific to this question only, and that's not really a good case for a new tag.Lipstick
@Charles: it is, indeed, a new tag. It's a quite new concept, too. Maybe there are tags for 'forwarding' etc..., but I didn't try look it up. I'll dig into it.Reasoning
G
7

If you have to store a const reference to some instance of type B into your class A, then surely you want to be ensured, that lifetime of A instance will be exceeded by the lifetime of B instance:

B b{};
A a1{b}; // allowed
A a2{B{}}; // should be denied
B const f() { return B{}; } // const result type may make sense for user-defined types
A a3{f()}; // should also be denied!

To make it possible you should explicitly to = delete; all the constructor overloadings, which can accept rvalues (both const && and &&). For this to achieve you should just to = delete; only const && version of constructor.

struct B {};

struct A
{
    B const & b;
    A(B const & bb) : b(bb) { ; } // accepts only `B const &` and `B &`
    A(B const &&) = delete; // prohibits both `B &&` and `B const &&`
};

This approach allows you to prohibit passing to the constructor all kinds of rvalues.

This also works for built-in scalars. For example, double const f() { return 0.01; }, though it cause a warning like:

warning: 'const' type qualifier on return type has no effect [-Wignored-qualifiers]

it still can has effect if you just = delete; only && version of constructor:

struct A
{
    double const & eps;
    A(double const & e) : eps(e) {} // binds to `double const &`, `double &` AND ! `double const &&`
    A(double &&) = delete; // prohibit to binding only to `double &&`, but not to `double const &&`
};

double const get_eps() { return 0.01; }

A a{0.01}; // hard error
A a{get_eps()}; // no hard error, but it is wrong!

For non-conversion constructors (i.e. non-unary) there is an issue: you may have to provide = delete;-d versions for all the combinatorically possible versions of constructors as follows:

struct A
{
    A(B const &, C const &) {}
    A(B const &&, C const &&) = delete;
    // and also!
    A(B const &, C const &&) = delete;
    A(B const &&, C const &) = delete;
};

to prohibit mixed-cases like:

B b{};
A a{b, C{}};
Groupie answered 23/12, 2016 at 13:6 Comment(2)
Good point, the multi arg constructors! Probably it would make sense to make all params templated, and statically assert all are lvalue references somehow. That would prevent mix-explosion.Reasoning
@Reasoning Simple static_assert is not a good choise, when you apply a type trait like std::is_constructible to A. You need to SFINAE-out bad combinations or to use Concept Lite if avaliable.Groupie
L
3

Ignoring the fact the code isn't valid and just answering the question about the private overload...

In C++11 I would prefer a deleted function to a private function. It's a bit more explicit that you really can't call it (not even if you're a member or friend of the class.)

N.B. if the deleted constructor is HasRef(int&&)=delete it will not be chosen here:

int i;
HasRef hr(std::forward<const int>(i));

With an argument of type const int&& the HasRef(const int&) constructor would be used, not the HasRef(int&&) one. In this case it would be OK, because i really is an lvalue, but in general that might not be the case, so this might be one of the very rare times when a const rvalue reference is useful:

HasRef(const int&&) = delete;
Larder answered 20/6, 2012 at 20:47 Comment(0)
G
2

That shouldn't compile. A good C++ compiler (or really almost any C++ compiler that I've ever seen) will stop that from happening.

Grandfather answered 20/6, 2012 at 20:42 Comment(1)
thanks; it seems that I have to take a better look at my code and rephrase the problem.Reasoning
M
0

I'm guessing you're compiling in MSVS. In that case, turn off language extensions and you should get an error.

Otherwise, not that even marking the reference const extends the lifetime of the temporary until the constructor finishes. After that, you'll refer to an invalid object.

Mutiny answered 20/6, 2012 at 20:56 Comment(4)
It is more easily said than done. Disabling languages extensions with MSVC makes their own standard library complain.Transmontane
@AlexandreC. I've had no problems so far... lucky?Mutiny
Probably. I recall having had problems with MSVC2005.Transmontane
My understanding is that the Standard Library headers should work under /Za, but the Windows SDK headers (Windows.h and friends) may not.Lymph

© 2022 - 2024 — McMap. All rights reserved.