指向堆栈分配对象和移动构造的指针

时间:2015-04-02 16:44:56

标签: c++ pointers move-semantics copy-elision rvo

注意:这是我刚刚发布的a question的完整重写措辞。如果您发现它们是重复的,请关闭另一个。

我的问题非常普遍,但似乎可以根据一个具体的简单例子更容易地解释它。 所以想象一下,我想模拟办公室的耗电量。让我们假设只有一个灯和暖气。

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

重点是:

1。Time无法移动为数据成员LightHeating,因为其更改不是来自任何这些类。

2. Time不必作为参数显式传递给Light。实际上,在程序的任何部分中都可能引用Light,而不想提供Time作为参数。

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;
}

现在,Simulation完全可以找到,只要它不是复制/移动构造。因为如果是,Light也将构造复制/移动,默认情况下,指向Time的指针将指向旧的Time实例中的Simulation,该实例将被复制/移动。 但是,在Simulation的return语句和SimulationBuilder::build()

中的对象创建之间main()实际复制/移动构造

现在有很多方法可以解决这个问题:

1:依靠复制省略。在这种情况下(以及在我的实际代码中)复制省略似乎是允许的标准。但不是必需,事实上,它是不是被clang -O3所省略。更确切地说,clang elides Simulation复制,但确实称之为Light的移动ctor。另请注意,依赖于与实现相关的时间并不健全。

2:在Simulation中定义一个move-ctor:

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;
}

这确实有效,但这里的一大问题是它削弱了封装:现在Simulation必须知道Light在移动过程中需要更多信息。在这个简化的例子中,这不是太糟糕,但是想象timePtr不是直接在Light中,而是在其子子成员之一。然后我必须写

Simulation::Simulation(Simulation&& old) 
    : time(old.time)
    , subStruct(old.subStruct)
{
    subStruct.getSubMember().getSubMember().getSubMember().resetTimePtr(&time);
}

完全打破了Demeter的封装和法则。即使在委派职能时我也觉得很糟糕。

3:使用某种观察者模式,Time正在观察Light,并在复制/移动构造时发送消息,以便Light在接收时更改其指针信息。 我必须承认我懒得写一个完整的例子,但我觉得它会很重,我不确定增加的复杂性是否值得。

4:在Simulation中使用拥有指针:

class Simulation {
    private:
        std::unique_ptr<Time> const time; // Note : heap-allocated
};

现在移动Simulation时,Time内存不是,因此Light中的指针不会失效。实际上,这是几乎所有其他面向对象语言的功能,因为所有对象都是在堆上创建的。 现在,我赞成这个解决方案,但仍然认为它并不完美:堆分配可能很慢,但更重要的是它只是似乎不是惯用的。我听说过B. Stroustrup说你不应该在不需要时使用指针而且需要或多或少的多态性。

5:就地构建Simulation,而不是SimulationBuilder返回(然后在Simulation中复制/移动ctor / assignment都可以删除)。例如

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.;
    }
};

现在我的问题如下:

1:你会用什么解决方案?你还想到另一个吗?

2:你觉得原设计有什么问题吗?你会怎么做才能解决它?

3:你有没有遇到过这种模式?我发现它在我的代码中很常见。但一般来说,这不是问题,因为Time确实是多态的,因此是堆分配的。

4:回到问题的根源,这是&#34;没有必要移动,我只想要就地创建一个不可移动的对象,但编译器不允许我这样做&#34;为什么C ++中没有简单的解决方案,是否有另一种语言的解决方案?

4 个答案:

答案 0 :(得分:2)

如果所有类都需要访问相同的const(因此也是不可变的)功能,那么您至少有2个选项可以使代码保持清洁和可维护:

  1. 存储SharedFeature而不是引用的副本 - 如果SharedFeature既小又无状态,这是合理的。

  2. 存储std::shared_ptr<const SharedFeature>而不是对const的引用 - 这适用于所有情况,几乎不需要额外费用。 std::shared_ptr当然是完全移动的。

答案 1 :(得分:1)

编辑:由于课程的命名和排序,我完全错过了你的两个课程无关的事实。

很难用#34; feature&#34;这样一个抽象的概念来帮助你。但我会在这里彻底改变我的想法。我建议将该功能的所有权移至MySubStruct。现在复制和移动将正常工作,因为只有MySubStruct知道它并且能够制作正确的副本。现在MyClass需要能够对功能进行操作。因此,只需在MySubStruct subStruct.do_something_with_feature(params);添加授权:

如果您的功能需要来自子结构和MyClass的数据成员,那么我认为您错误地分配了职责,需要重新考虑回到MyClassMySubStruct的分割。

原始答案基于MySubStructMyClass的孩子的假设:

我认为正确的答案是从子项中删除featurePtr并为父项中的功能提供适当的受保护接口(注意:我确实在这里指的是一个抽象接口,而不仅仅是get_feature()功能)。然后,父母不必知道孩子,孩子可以根据需要操作该功能。

要完全明确:MySubStruct 不会知道父类甚至是名为feature的成员。例如,可能是这样的:

答案 2 :(得分:1)

  

1:你会用什么解决方案?你还想到另一个吗?

为什么不应用一些设计模式?我在您的解决方案中看到了工厂和单件的用途。可能还有其他一些我们可以申请工作但我在模拟过程中应用工厂的经验比其他任何方式更有经验。

  • 模拟变成单身人士。

build()中的SimulationBuilder功能已移至SimulationSimulation的构造函数被私有化,您的主要调用变为Simulation * builder = Simulation::build();。模拟还会获得一个新变量static Simulation * _Instance;,我们会对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;
  • 灯光和暖气对象作为工厂提供。

如果您在模拟中只有2个对象,那么这个想法就毫无价值。但是,如果您要管理1 ... N个对象和多种类型,那么我强烈建议您使用工厂和动态列表(矢量,双端队列等)。您需要使Light和Heating继承自公共模板,设置为在工厂中注册这些类,设置工厂以使其模板化,工厂实例只能创建特定模板的对象,并初始化Simulation对象的工厂。基本上工厂看起来像这样

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;

这可能看起来有点奇怪,但它确实让创建模板对象变得有趣。您可以通过以下方式注册它们:

// To load a product we would call it like this:
pFactory.get()->Register<Light>("Light");
pFactory.get()->Register<Heating>("Heating");

然后当你需要实际获得一个对象时,你只需要:

std::shared_ptr<Light> light = pFactory.get()->GetProduct("Light");

  

2:你觉得原设计有什么问题吗?你会怎么做才能解决它?

是的,我当然这样做,但不幸的是,从我对第1项的回答中我没有太多要说明。

如果我要修理任何东西,我会开始修理&#34;通过查看性能分析会话告诉我的内容。如果我担心时间分配内存等问题,那么分析是了解分配时间长度的最佳方法。当你不重用已知的配置文件实现时,世界上所有的理论都无法弥补分析。

另外,如果我真的担心内存分配等事情的速度,那么我会考虑我的分析运行中的事情,例如创建对象的次数与对象的生命时间,希望我的分析会议告诉我这个。对于给定的模拟运行,最多应创建一个类似Simulation类的对象,而在运行期间可能会创建0 {N}个对象,如Light。因此,我将重点关注创建Light对象如何影响我的表现。


  3:你有没有遇到过这种模式?我发现它在我的代码中很常见。一般来说,这不是问题,因为Time确实是多态的,因此堆分配。

我通常不会看到模拟对象保持一种方式来查看当前的状态变化变量,例如Time。我通常会看到一个对象保持其状态,并且只有在通过SetState(Time & t){...}等函数发生时间更改时才会更新。如果你考虑一下,那就有意义了。模拟是一种在给定特定参数的情况下查看对象变化的方法,并且对象不应该需要该参数来报告其状态。因此,对象应该只通过单个函数更新并在函数调用之间保持其状态。

// 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;
}

保持一个对象在时间上执行自己的检查有助于缓解多线程压力,例如&#34;如何处理时间变化的情况,并在我读完时间后使我的开/关状态无效,但在我返回之前我的州?&#34;


  

4:回到问题的根源,这是&#34;没有必要移动,我只想要就地创建一个不可移动的对象,但编译器不允许我这样做&#34;为什么C ++中没有简单的解决方案,是否有另一种语言的解决方案?

如果您只想创建1个对象,则可以使用Singleton Design Pattern。正确实现后,即使在多线程场景中,Singleton也能保证只生成一个对象实例。

答案 3 :(得分:1)

在对第二个解决方案的评论中,您说它削弱了封装,因为Simulation必须知道Light在移动期间需要更多信息。我认为这是相反的方式。 Light需要知道在Time生命期内提供的Light对象引用可能无效的上下文中使用的Light。哪个不好,因为它强制设计Simulation基于它的使用方式,而不是基于它应该做什么。

在两个对象之间传递引用会创建(或应该创建)它们之间的契约。传递对函数的引用时,该引用应该有效,直到被调用的函数返回为止。传递对象构造函数的引用时,该引用应在构造对象的整个生命周期内有效。传递引用的对象负责其有效性。如果不遵循这一点,则可能会非常难以跟踪引用用户与维护引用对象生命周期的实体之间的关系。在您的示例中,Light无法维护它与移动时创建的Light对象之间的契约。由于Simulation对象的生命周期与Time对象的生命周期紧密耦合,因此有三种方法可以解决此问题:

1)您的解决方案编号2)

2)将对Simulation对象的引用传递给Simulation的构造函数。如果您认为Simulation和传递引用的外部实体之间的合同是可靠的,那么LightSimulation之间的合同也是可靠的。但是,您可以将Time对象视为Simulation对象的内部细节,从而打破封装。

3)使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; } 无法移动。由于C ++(11/14)没有任何“就地构造函数方法”(不知道术语有多好),因此无法通过从某个函数返回来创建就地对象。 Copy / Move-elision目前是一种优化,而不是一项功能。为此,您可以使用解决方案5)或使用lambdas,如下所示:

{{1}}

如果没有一个符合您的需求,那么您要么必须重新评估您的需求(也可能是一个不错的选择),或者在某处使用堆分配和指针。