Reputation: 260
I'm getting dangling references while using a ranged-for loop. Consider the following C++14 expression (full example program below):
for(auto& wheel: Bike().wheels_reference())
wheel.inflate();
It's output is:
Wheel()
Wheel()
Bike()
~Bike() with 0 inflated wheels.
~Wheel()
~Wheel()
Wheel::inflate()
Wheel::inflate()
Obviously something is going very wrong. Wheels are accessed beyond their lifetime and the result is 0, not the expected 2.
An easy fix is to introduce a variable for Bike
in main
. However, I do not control the code in main
or Wheel
. I can only change the struct Bike
.
Is there any way to fix this example by only changing Bike
?
A successful solution solution will either fail at compile time, or count 2 inflated tires and not touch any objects beyond their lifetimes.
#include <cstdlib>
#include <iostream>
#include <array>
#include <algorithm>
using std::cout;
using std::endl;
struct Wheel
{
Wheel() { cout << " Wheel()" << endl; }
~Wheel() { cout << "~Wheel()" << endl; }
void inflate() { inflated = true; cout << " Wheel::inflate()" << endl; }
bool inflated = false;
};
struct Bike
{
Bike() { cout << " Bike()" << endl; }
~Bike() {
cout << "~Bike() with " << std::count_if(wheels.begin(), wheels.end(),
[](auto& w) { return w.inflated; }) << " inflated wheels." << endl;
}
std::array<Wheel, 2>& wheels_reference() { return wheels; }
std::array<Wheel, 2> wheels{Wheel(), Wheel()};
};
int main()
{
for(auto& wheel: Bike().wheels_reference())
wheel.inflate();
return EXIT_SUCCESS;
}
Upvotes: 7
Views: 294
Reputation: 16731
You can just add a couple of cv-ref-qualified overloadings of wheels_reference
member function:
std::array<Wheel, 2>& wheels_reference() & { return wheels; }
std::array<Wheel, 2> const & wheels_reference() const & { return wheels; }
std::array<Wheel, 2> wheels_reference() && { return std::move(wheels); }
std::array<Wheel, 2> wheels_reference() const && { return wheels; }
Note, if object is temporary (&&
case), then you should return a value (copy-constructed from data member or even better move-constructed from it), nor reference.
Having all four overloadings you cover all the use-cases possible.
Upvotes: 1
Reputation: 473916
The best solution is to stop getting members of a type through a member function call on temporary.
If wheels_reference
were a non-member function, you could simply declare it like this:
wheels_reference(Bike &bike);
Since a non-const lvalue parameter cannot be attached to a temporary, you would be unable to call wheels_reference(Bike())
. Since wheels_reference
is a member function, you'll just have to use the member function syntax for saying the same thing:
std::array<Wheel, 2>& wheels_reference() & //<--
{ return wheels; }
If the user now tries to call Bike().wheels_reference()
, the compiler will complain.
Upvotes: 3
Reputation: 2281
Delete the rvalue overload of wheels_reference
.
std::array<Wheel, 2>& wheels_reference() & { return wheels; }
std::array<Wheel, 2>& wheels_reference() && = delete;
That way you won't return a reference to a member of a temporary.
Your example usage of the Bike
class
for(auto& wheel: Bike().wheels_reference())
wheel.inflate();
Will then refuse to compile with (clang 3.4 output):
test.cpp:31:29: error: call to deleted member function 'wheels_reference'
for(auto& wheel: Bike().wheels_reference())
~~~~~~~^~~~~~~~~~~~~~~~
test.cpp:24:27: note: candidate function has been explicitly deleted
std::array<Wheel, 2>& wheels_reference() && = delete;
^
test.cpp:23:27: note: candidate function not viable: no known conversion from 'Bike' to 'Bike' for object argument
std::array<Wheel, 2>& wheels_reference() & { return wheels; }
If the lifetime of the temporary is manually extended things work.
Bike&& bike = Bike();
for(auto& wheel: bike.wheels_reference())
wheel.inflate();
Upvotes: 6
Reputation: 260
The following horrible contraption seems to satisfy all the conditions:
#include <memory>
struct Bike
{
// Bike() { cout << " FakeBike()" << endl; }
// ~Bike() { cout << "~FakeBike()" << endl; }
struct RealBike;
struct Wrap {
std::shared_ptr<RealBike> parent;
auto begin() { return parent->wheels.begin(); }
auto end() { return parent->wheels.end(); }
};
struct RealBike {
RealBike() { cout << " Bike()" << endl; }
~RealBike() {
cout << "~Bike() with " << std::count_if(wheels.begin(), wheels.end(),
[](auto& w) { return w.inflated; }) << " inflated wheels." << endl;
}
std::array<Wheel, 2> wheels;
};
std::shared_ptr<RealBike> real = std::make_shared<RealBike>();
Wrap wheels_reference() { return Wrap{real}; }
};
What I don't like is that it requires wrapping all the API of std::array<Wheel, 2>
in Wrap
.
Upvotes: 1