Note: This is a complete re-wording of a question I posted a while ago. If you find they are duplicate, please close the other one.
My problem is quite general but it seems that it could be explained more easily based on a concrete simple example. So imagine I want to simulate the electricity consumption in an office throught time. Let's assume that there is only a light and heating.
class Simulation {
public:
Simulation(Time const& t, double lightMaxPower, double heatingMaxPower)
: time(t)
, light(&time,lightMaxPower)
, heating(&time,heatingMaxPower) {}
private:
Time time; // Note : stack-allocated
Light light;
Heating heating;
};
class Light {
public:
Light(Time const* time, double lightMaxPower)
: timePtr(time)
, lightMaxPower(lightMaxPower) {}
bool isOn() const {
if (timePtr->isNight()) {
return true;
} else {
return false;
}
}
double power() const {
if (isOn()) {
return lightMaxPower;
} else {
return 0.;
}
private:
Time const* timePtr; // Note : non-owning pointer
double lightMaxPower;
};
// Same kind of stuff for Heating
The important points are:
1.Time
cannot be moved to be a data member Light
or Heating
since its change does not come from any of these classes.
2.Time
does not have to be explicitly passed as a parameter to Light
. Indeed, there could be a reference to Light
in any part of the program that does not want to provide Time
as a parameter.
class SimulationBuilder {
public:
Simulation build() {
Time time("2015/01/01-12:34:56");
double lightMaxPower = 42.;
double heatingMaxPower = 43.;
return Simulation(time,lightMaxPower,heatingMaxPower);
}
};
int main() {
SimulationBuilder builder;
auto simulation = builder.build();
WeaklyRelatedPartOfTheProgram lightConsumptionReport;
lightConsumptionReport.editReport((simulation.getLight())); // No need to supply Time information
return 0;
}
Now, Simulation
is perfectly find as long as it is not copy/move constructed. Because if it is, Light
will also get copy/move constructed and by default, the pointer to Time will be pointing to the Time
in the old Simulation
instance which is copied/moved from.
However, Simulation
actually is copy/move constructed in between the return statement in SimulationBuilder::build()
and the object creation in main()
Now there are a number of ways to solve the problem:
1: Rely on copy elision. In this case (and in my real code) copy elision seems to be allowed by the standard. But not required, and as a matter of fact, it is not elided by clang -O3. To be more precise, clang elides Simulation
copy, but does call the move ctor for Light
. Also notice that relying on an implementation-dependent time is not robust.
2: Define a move-ctor in Simulation
:
Simulation::Simulation(Simulation&& old)
: time(old.time)
, light(old.light)
, heating(old.heating)
{
light.resetTimePtr(&time);
heating.resetTimePtr(&time);
}
Light::resetTimePtr(Time const* t) {
timePtr = t;
}
This does work but the big problem here is that it weakens encapsulation: now Simulation
has to know that Light
needs more info during a move. In this simplified example, this is not too bad, but imagine timePtr
is not directly in Light
but in one of its sub-sub-sub-member. Then I would have to write
Simulation::Simulation(Simulation&& old)
: time(old.time)
, subStruct(old.subStruct)
{
subStruct.getSubMember().getSubMember().getSubMember().resetTimePtr(&time);
}
which completly breaks encapsulation and the law of Demeter. Even when delegating functions I find it horrible.
3: Use some kind of observer pattern where Time
is being observed by Light
and sends a message when it is copy/move constructed so that Light
change its pointer when receiving the message.
I must confess I am lazy to write a complete example of it but I think it will be so heavy I am not sure the added complexity worth it.
4: Use a owning pointer in Simulation
:
class Simulation {
private:
std::unique_ptr<Time> const time; // Note : heap-allocated
};
Now when Simulation
is moved, the Time
memory is not, so the pointer in Light
is not invalidated. Actually this is what almost every other object-oriented language does since all objects are created on the heap.
For now, I favor this solution, but still think it is not perfect: heap-allocation could by slow, but more importantly it simply does not seems idiomatic. I've heard B. Stroustrup say that you should not use a pointer when not needed and needed meant more or less polymorphic.
5: Construct Simulation
in-place, without it being return by SimulationBuilder
(Then copy/move ctor/assignment in Simulation
can then all be deleted). For instance
class Simulation {
public:
Simulation(SimulationBuilder const& builder) {
builder.build(*this);
}
private:
Time time; // Note : stack-allocated
Light light;
Heating heating;
...
};
class SimulationBuilder {
public:
void build(Simulation& simulation) {
simulation.time("2015/01/01-12:34:56");
simulation.lightMaxPower = 42.;
simulation.heatingMaxPower = 43.;
}
};
Now my questions are the following:
1: What solution would you use? Do you think of another one?
2: Do you think there is something wrong in the original design? What would you do to fix it?
3: Did you ever came across this kind of pattern? I find it rather common throughout my code. Generally though, this is not a problem since Time
is indeed polymorphic and hence heap-allocated.
4: Coming back to the root of the problem, which is "There is no need to move, I only want an unmovable object to be created in-place, but the compiler won't allow me to do so" why is there no simple solution in C++ and is there a solution in another language ?