Bérenger
Bérenger

Reputation: 2748

Pointers to stack-allocated object and move-contruction

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

Answers (4)

Dalibor Frivaldsky
Dalibor Frivaldsky

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

v4n3ck
v4n3ck

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.

  • Simulation turns into a Singleton.

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;
  • Light and Heating objects get provided as a Factory.

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

Richard Hodges
Richard Hodges

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:

  1. store copies of the SharedFeature rather than references - this is reasonable if SharedFeature is both small and stateless.

  2. 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

Mark B
Mark B

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

Related Questions