Reputation: 2748
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 ?
Upvotes: 4
Views: 1131
Reputation: 850
In the comment to your second solution, you're saying that it weakens the encapsulation, because the Simulation
has to know that Light
needs more information during move. I think it is the other way around. The Light
needs to know that is being used in a context where the provided reference to the Time
object may become invalid during Light
's lifetime. Which is not good, because it forces a design of Light
based on how it is being used, not based on what it should do.
Passing a reference between two objects creates (or should create) a contract between them. When passing a reference to a function, that reference should be valid until the function being called returns. When passing a reference to an object constructor, that reference should be valid throughout the lifetime of the constructed object. The object passing a reference is responsible for its validity. If you don't follow this, you may create very hard to trace relationships between the user of the reference and an entity maintaining lifetime of the referenced object. In your example, the Simulation
is unable to uphold the contract between it and the Light
object it creates when it is moved. Since the lifetime of the Light
object is tightly coupled to the lifetime of the Simulation
object, there are 3 ways to resolve this:
1) your solution number 2)
2) pass a reference to the Time
object to constructor of the Simulation
. If you assume the contract between the Simulation
and the outer entity passing the reference is reliable, so will be the contract between Simulation
and Light
. You may, however, consider the Time object to be internal detail of the Simulation
object and thus you would break encapsulation.
3) make the Simulation
unmovable. Since C++(11/14) does not have any "in-place constructor methods" (don't know how good a term that is), you cannot create an in-place object by returning it from some function. Copy/Move-elision is currently an optimalization, not a feature. For this, you can either use your solution 5) or use lambdas, like this:
class SimulationBuilder {
public:
template< typename SimOp >
void withNewSimulation(const SimOp& simOp) {
Time time("2015/01/01-12:34:56");
double lightMaxPower = 42.;
double heatingMaxPower = 43.;
Simulation simulation(time,lightMaxPower,heatingMaxPower);
simOp( simulation );
}
};
int main() {
SimulationBuilder builder;
builder.withNewSimulation([] (Simulation& simulation) {
WeaklyRelatedPartOfTheProgram lightConsumptionReport;
lightConsumptionReport.editReport((simulation.getLight())); // No need to supply Time information
}
return 0;
}
If none fits your needs, then you either have to reevaluate your needs (might be a good option, too) or use heap allocation and pointers somewhere.
Upvotes: 1
Reputation: 51
1: What solution would you use? Do you think of another one?
Why not apply a few design patterns? I see uses for a factory and a singleton in your solution. There are probably a few others that we could claim work but I am way more experienced with applying a Factory during a simulation than anything else.
The build()
function in SimulationBuilder
gets moved into Simulation
. The constructor for Simulation
gets privatized, and your main call becomes Simulation * builder = Simulation::build();
. Simulation also gets a new variable static Simulation * _Instance;
, and we make a few changes to Simulation::build()
class Simulation
{
public:
static Simulation * build()
{
// Note: If you don't need a singleton, just create and return a pointer here.
if(_Instance == nullptr)
{
Time time("2015/01/01-12:34:56");
double lightMaxPower = 42.;
double heatingMaxPower = 43.;
_Instance = new Simulation(time, lightMaxPower, heatingMaxPower);
}
return _Instance;
}
private:
static Simulation * _Instance;
}
Simulation * Simulation::_Instance = nullptr;
This thought is worthless if you are only going to have 2 objects inside of simulation. But, if you are going to be managing 1...N objects and multiple types, then I would strongly encourage you utilize a factory, and a dynamic list (vector, deque, etc.). You would need to make Light and Heating inherit from a common template, set things up to register those classes with the factory, set the factory so that it is templated and an instance of the factory can only create objects of a specific template, and initialize the factory for the Simulation object. Basically the factory would look something like this
template<class T>
class Factory
{
// I made this a singleton so you don't have to worry about
// which instance of the factory creates which product.
static std::shared_ptr<Factory<T>> _Instance;
// This map just needs a key, and a pointer to a constructor function.
std::map<std::string, std::function< T * (void)>> m_Objects;
public:
~Factory() {}
static std::shared_ptr<Factory<T>> CreateFactory()
{
// Hey create a singleton factory here. Good Luck.
return _Instance;
}
// This will register a predefined function definition.
template<typename P>
void Register(std::string name)
{
m_Objects[name] = [](void) -> P * return new P(); };
}
// This could be tweaked to register an unknown function definition,
void Register(std::string name, std::function<T * (void)> constructor)
{
m_Objects[name] = constructor;
}
std::shared_ptr<T> GetProduct(std::string name)
{
auto it = m_Objects.find(name);
if(it != m_Objects.end())
{
return std::shared_ptr<T>(it->second());
}
return nullptr;
}
}
// We need to instantiate the singleton instance for this type.
template<class T>
std::shared_ptr<Factory<T>> Factory<T>::_Instance = nullptr;
That may seem a bit weird, but it really makes creating templated objects fun. You can register them by doing this:
// To load a product we would call it like this:
pFactory.get()->Register<Light>("Light");
pFactory.get()->Register<Heating>("Heating");
And then when you need to actually get an object all you need is:
std::shared_ptr<Light> light = pFactory.get()->GetProduct("Light");
2: Do you think there is something wrong in the original design? What would you do to fix it?
Yeah I certainly do, but unfortunately I don't have much to expound upon from my answer to item 1.
If I were to fix anything I start "fixing" by seeing what a Profiling session tells me. If I was worried about things like time to allocate memory, then profiling is the best way to get an accurate idea about how long to expect allocations to take. All the theories in the world cannot make up for profiling when you are not reusing known profiled implementations.
Also, if I were truly worried about the speed of things like memory allocation then I would take into consideration things from my profiling run such as the number of times that an object is created vs the objects time of life, hopefully my profiling session told me this. An object like your Simulation
class should be created at most 1 time for a given simulation run while an object like Light
might be created 0..N times during the run. So, I would focus on how creating Light
objects affected my performance.
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.
I do not typically see simulation objects maintain a way to see the current state change variables such as Time
. I typically see an object maintain its state, and only update when a time change occurs through a function such as SetState(Time & t){...}
. If you think about it, that kind of makes sense. A simulation is a way to see the change of objects given a particular parameter(s), and the parameter should not be required for the object to report its state. Thus, an object should only update by single function and maintain its state between function calls.
// This little snippet is to give you an example of how update the state.
// I guess you could also do a publish subscribe for the SetState function.
class Light
{
public:
Light(double maxPower)
: currPower(0.0)
, maxPower(maxPower)
{}
void SetState(const Time & t)
{
currentPower = t.isNight() ? maxPower : 0.0;
}
double GetCurrentPower() const
{
return currentPower;
}
private:
double currentPower;
double maxPower;
}
Keeping an object from performing its own check on Time helps alleviate multithreaded stresses such as "How do I handle the case where the time changes and invalidates my on/off state after I read the time, but before I returned my state?"
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 ?
If you only ever want 1 object to be created you can use the Singleton Design Pattern. When correctly implemented a Singleton is guaranteed to only ever make 1 instance of an object, even in a multithreaded scenario.
Upvotes: 1
Reputation: 69892
If all classes need access to the same const (and therefore immutable) feature, you have (at least) 2 options to make the code clean and maintainable:
store copies of the SharedFeature
rather than references - this is reasonable if SharedFeature
is both small and stateless.
store a std::shared_ptr<const SharedFeature>
rather than a reference to const - this works in all cases, with almost no additional expense. std::shared_ptr
is of course fully move-aware.
Upvotes: 2
Reputation: 96251
EDIT: Due to the class naming and ordering I completely missed the fact that your two classes are unrelated.
It's really hard to help you with such an abstract concept as "feature" but I'm going to completely change my thought here. I would suggest moving the feature's ownership into MySubStruct
. Now copying and moving will work fine because only MySubStruct
knows about it and is able to make the correct copy. Now MyClass
needs to be able to operate on feature. So, where needed just add delegation to MySubStruct
: subStruct.do_something_with_feature(params);
.
If your feature needs data members from both sub struct AND MyClass
then I think you split responsibilities incorrectly and need to reconsider all the way back to the split of MyClass
and MySubStruct
.
Original answer based on the assumption that MySubStruct
was a child of MyClass
:
I believe the correct answer is to remove featurePtr
from the child and provide a proper protected interface to feature in the parent (note: I really do mean an abstract interface here, not just a get_feature()
function). Then the parent doesn't have to know about children and the child can operator on the feature as needed.
To be completely clear: MySubStruct
will not know that the parent class even HAS a member called feature
. For example, perhaps something like this:
Upvotes: 1