我的问题很普遍,但似乎通过一个具体简单的例子更容易解释。因此,想象一下我想模拟办公室用电量随时间变化的情况。假设只有照明和供暖。
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.
时间
不能被移动到数据成员Light
或Heating
,因为它的变化并不来自这些类中的任何一个。2.
时间
不必作为参数明确传递给Light
。实际上,在程序的任何部分都可以有对Light
的引用,而不想将时间
作为参数提供。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
是完全OK的。因为如果这样做了,Light
也会被复制/移动构造,并且默认情况下,指向Time
的指针将指向从中复制/移动的旧Simulation
实例中的Time
。然而,在SimulationBuilder :: build()
的return语句之间和main()
中的对象创建之间,Simulation
实际上被复制/移动构造了。现在有许多解决问题的方法:
1:依靠复制省略。在这种情况下(在我的真实代码中也是如此),标准似乎允许复制省略。但是不是必需的,事实上,clang-O3没有省略。更确切地说,clang省略了
Simulation
的复制,但确实调用了Light
的移动构造函数。还请注意,依赖于实现相关的时间不够健壮。2:在
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);
}
这完全破坏了封装和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
中的复制/移动构造函数/赋值)。例如。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++中没有简单的解决方案?其他语言中是否有解决方案?
MySubStruct
中的引用是const)。您使用共享指针的解决方案类似于我的解决方案#4:还不错,但仍然不完全令人满意,因为没有必要进行堆分配。 - BérengerMyClass
中保留一个常规的SharedFeature
,并在MySubStruct
、MyOtherSubStruct
中创建一个指向MyClass
中的feature
的std::shared_ptr<const SharedFeature>
和无操作删除器?但是当移动MyClass
时,feature
也会被移动,我不知道这些shared_ptrs如何知道它的新地址。 - Bérenger