4

注意:这是对我不久前发布的问题的完整重新措辞。如果您发现它们重复,请关闭另一个。

我的问题很笼统,但似乎可以根据一个具体的简单示例更容易地解释它。所以想象一下我想模拟办公室的用电量。让我们假设只有光和加热。

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不能移动为数据成员Light,或者Heating因为它的更改不是来自这些类中的任何一个。

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的旧Simulation实例中的。然而,Simulation实际上SimulationBuilder::build()在返回语句和对象创建之间构建的复制/移动main()

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

1:依靠复制省略。在这种情况下(在我的真实代码中),标准似乎允许复制省略。但不是必需的,事实上,它并没有被 clang -O3 忽略。更准确地说,clang 省略Simulation了复制,但确实将移动 ctor 称为Light. 另请注意,依赖于实现相关的时间并不可靠。

2:在中定义一个move-ctor 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;
}

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

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

这完全打破了封装和得墨忒耳定律。即使在委派职能时,我也觉得这很可怕。

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

4:在中使用拥有指针Simulation

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

现在当Simulation被移动时,Time内存不是,所以指针 inLight不会失效。实际上这是几乎所有其他面向对象语言所做的,因为所有对象都是在堆上创建的。目前,我赞成这个解决方案,但仍然认为它并不完美:堆分配可能会很慢,但更重要的是它看起来并不习惯。我听说 B. Stroustrup 说你不应该在不需要时使用指针,而需要意味着或多或少是多态的。

Simulation5:就地构造,不被返回SimulationBuilder(然后复制/移动ctor/assignmentSimulation可以全部删除)。例如

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:回到问题的根源,即“不需要移动,我只想在原地创建一个不可移动的对象,但编译器不允许我这样做”为什么没有C++ 中的简单解决方案,是否有另一种语言的解决方案?

4

4 回答 4

2

如果所有类都需要访问相同的 const(因此是不可变的)特性,那么您(至少)有 2 个选项可以使代码干净且可维护:

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

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

于 2015-04-02T22:17:00.980 回答
1

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

为什么不应用一些设计模式?我在您的解决方案中看到了工厂和单身人士的用途。可能还有其他一些我们可以声称的工作,但我在模拟过程中应用工厂的经验比其他任何事情都多。

  • 模拟变成了单例。

中的build()函数SimulationBuilder被移入Simulation. 的构造函数Simulation被私有化,您的主要调用变为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;
  • Light 和 Heating 对象作为工厂提供。

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

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 项的回答没有太多可解释的。

如果我要修复任何东西,我会通过查看Profiling会话告诉我的内容来开始“修复”。如果我担心分配内存的时间等问题,那么分析是准确了解预期分配需要多长时间的最佳方法。当您不重用已知的分析实现时,世界上所有的理论都无法弥补分析。

另外,如果我真的担心内存分配之类的速度,那么我会考虑我的分析运行中的一些事情,例如创建对象的次数与对象的生命周期,希望我的分析会话告诉我这个。对于给定的模拟运行,像您的Simulation类这样的对象最多应该创建 1 次,而在Light运行期间可能会创建 0..N 次这样的对象。因此,我将专注于创建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;
}

阻止对象对时间执行自己的检查有助于减轻多线程压力,例如“我如何处理在读取时间之后但在返回状态之前时间发生变化并使我的开/关状态无效的情况?”


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

如果您只想创建 1 个对象,则可以使用单例设计模式。正确实现时,即使在多线程场景中,也可以保证 Singleton 仅生成 1 个对象实例。

于 2015-04-08T23:50:55.190 回答
1

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

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

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

Time2) 将对对象的引用传递给Simulation. 如果您假设Simulation和 传递引用的外部实体之间的合同是可靠的,那么Simulation和之间的合同也是如此Light。但是,您可能会认为 Time 对象是对象的内部细节,Simulation因此您会破坏封装。

3)使不动Simulation。由于 C++(11/14) 没有任何“就地构造方法”(不知道这个术语有多好),因此您无法通过从某个函数返回来创建就地对象。复制/移动省略目前是一种优化,而不是一项功能。为此,您可以使用解决方案 5) 或使用 lambda,如下所示:

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

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

于 2015-04-10T14:22:13.217 回答
1

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

用“特征”这样抽象的概念来帮助你真的很难,但我要在这里彻底改变我的想法。我建议将该功能的所有权转移到MySubStruct. 现在复制和移动可以正常工作,因为只MySubStruct知道它并且能够制作正确的副本。现在MyClass需要能够对特征进行操作。因此,在需要的地方只需将委托添加到MySubStruct: subStruct.do_something_with_feature(params);

如果您的功能需要来自两个子结构 AND 的数据成员,MyClass那么我认为您错误地拆分了职责,需要重新考虑一直到拆分MyClassand MySubStruct

基于以下假设的原始MySubStruct答案MyClass

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

要完全清楚:MySubStruct 不会知道父类甚至有一个名为feature. 例如,也许是这样的:

于 2015-04-02T16:56:38.850 回答