注意:这是对我不久前发布的问题的完整重新措辞。如果您发现它们重复,请关闭另一个。
我的问题很笼统,但似乎可以根据一个具体的简单示例更容易地解释它。所以想象一下我想模拟办公室的用电量。让我们假设只有光和加热。
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 说你不应该在不需要时使用指针,而需要意味着或多或少是多态的。
Simulation
5:就地构造,不被返回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++ 中的简单解决方案,是否有另一种语言的解决方案?