C++ 在销毁派生成员之前调用析构函数中的公共函数
假设我们有这样的经典基类和派生类C++ 在销毁派生成员之前调用析构函数中的公共函数,c++,c++11,C++,C++11,假设我们有这样的经典基类和派生类 B类{ 公众: 虚拟~B(){ //在这里打电话太晚了,请参见解释 //公共_pre_cleanup_函数(); } void common_pre_cleanup_函数(){} }; D类:公共B类{ 公众: 虚拟~D(){ //如果我们忘记在另一个派生类中执行此调用怎么办? 公共_pre_cleanup_函数(); } }; 如何确保在销毁D的成员之前,在所有派生的Ds析构函数中调用像common\u pre\u cleanup\u function()这
B类{
公众:
虚拟~B(){
//在这里打电话太晚了,请参见解释
//公共_pre_cleanup_函数();
}
void common_pre_cleanup_函数(){}
};
D类:公共B类{
公众:
虚拟~D(){
//如果我们忘记在另一个派生类中执行此调用怎么办?
公共_pre_cleanup_函数();
}
};
如何确保在销毁D
的成员之前,在所有派生的D
s析构函数中调用像common\u pre\u cleanup\u function()
这样的函数,而不必在新D
的每个析构函数实现中显式调用此函数
背景
在我当前的项目中,我们有一个基类,它实现了某些并行性和线程特性,并最终将启动一个执行实际工作的新线程。
在这个基类的析构函数中,我们希望确保线程始终停止并连接,以便正确地清理它
但是,派生类可以在基类中创建此线程使用的成员。所以,如果我们销毁派生类的对象,这些成员也会被销毁。但此时,由基类管理的线程仍然可以运行,并且现在可以错误地访问被破坏的成员
我知道这不是解决这个问题的最聪明的方法,可能将线程/并行化部分和“实际工作”部分拆分为单独的类是更聪明的想法。然而,我感兴趣的是,是否有任何方法不涉及对现有代码库的全部重写
这里的代码更接近我们的情况
类背景任务{
公众:
虚拟背景任务(){
//如果我们忘记在派生类中调用stop(),我们将
//此时,已销毁任何派生成员
//而线程可能仍在运行并访问它们;那么如何/在哪里
//我们能打这个电话吗?
//停止();
}
无效停止(){
cancelFlag_uzy.set();
螺纹连接();
}
//更多有助于完成后台任务的功能
私人:
螺纹螺纹;
条件取消标志;
};
类MyTask:公共背景任务{
公众:
虚拟任务{
//就目前的情况而言,我们必须记得打电话
//此函数用于派生类中的所有析构函数
//从背景任务;这是我想要避免的
停止();
}
私人:
std::唯一的ptr成员;
};
编辑:我意识到这并不能回答你的问题,但我会把它留在这里作为参考。
如前所述,每个对象都应该负责管理自己的资源,因此您的设计从一开始就有点缺陷
考虑下面的例子。TaskRunner
负责启动线程,并在调用构造函数时将其关闭(i)。Task
类通过纯虚拟继承指定任务生命周期内要执行的操作
#include <atomic>
#include <future>
#include <iostream>
#include <memory>
struct Task {
virtual void run( ) = 0;
virtual ~Task( ) {
}
};
class TaskRunner final {
std::unique_ptr<Task> task;
std::future<void> fut;
std::atomic<bool> terminate;
public:
TaskRunner(std::unique_ptr<Task>&& task)
: task {std::move(task)}
, terminate {false} {
fut = std::async(std::launch::async, [this] {
while(!terminate) {
this->task->run( );
}
this->task.reset( );
});
}
TaskRunner(TaskRunner&&) = delete;
TaskRunner& operator=(TaskRunner&&) = delete;
TaskRunner(const TaskRunner&) = delete;
TaskRunner& operator=(const TaskRunner&) = delete;
~TaskRunner( ) {
terminate = true;
fut.wait( ); // Block until cleanup is completed
std::cout << "~TaskRunner()" << std::endl;
}
};
struct MyTask : public Task {
int i = 0;
void
run( ) {
// Do important stuf here, don't block.
std::cout << "MyTask::run() " << i++ << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds {100});
}
~MyTask( ) override {
// Clean up stuff here, run() is guaranteed to never be run again
std::cout << "~MyTask()" << std::endl;
}
};
int
main( ) {
TaskRunner t {std::make_unique<MyTask>( )};
std::this_thread::sleep_for(std::chrono::seconds {1});
}
很简单,你没有。在这种情况下,最好的办法是重新设计一切工作方式,以防止出现问题 但让我们面对现实吧,很可能你没有时间和/或资源来实现这一目标。所以,您的第二个最佳选择(在我看来)是确保对派生类的已销毁成员的任何调用都会立即终止您的应用程序,并显示一条非常清晰的错误消息
如果一个系统必须失败,那就早点失败。尽管我同意设计有缺陷的评论 假设对象是动态分配的,一种解决方案是使析构函数
虚拟
和受保护
,并使用单独的函数在销毁对象之前调用“预清理”。比如,
class B
{
public:
void die()
{
common_pre_cleanup_function();
delete this;
};
protected:
virtual ~B() {};
private:
void common_pre_cleanup_function() { };
};
class D : public B
{
protected:
virtual ~D() {};
};
int main()
{
B *b = new D;
b->die();
}
这对类的用户有一些限制。特别是,如果
新表达式创建的李>
调用die()
调用die()
这也意味着,如果您维护一组对象(如指针向量,B*
),则有必要从列表中删除指针,以确保对象死后不会被使用
受保护的
析构函数阻止了一些事情。不是B
或D
的friend
s成员的函数不能执行
创建自动存储持续时间的B
或D
直接使用操作员delete
。例如,一条语句deleteb上面main()
中的code>将不会编译。这还可以防止在调用“预清理”之前销毁对象
您可能会这样做:
template <typename TaskImpl>
class Task final : public TaskImpl
{
static_assert(std::is_base_of<BackgroundTask, TaskImpl>);
public:
virtual ~Task() { stop(); }
};
模板
类任务最终版本:public taskinpl
{
静态断言(std::is_base_of);
公众:
虚拟~Task(){stop();}
};
然后
class MyTaskImpl : public BackgroundTask
{
// ...
private:
std::unique_ptr<MyClass> member;
};
using MyTask = Task<MyTaskImpl>;
类MyTaskImpl:公共背景任务
{
// ...
私人:
std::唯一的ptr成员;
};
使用MyTask=Task;
“将线程/并行化部分和“实际工作”部分拆分为单独的类可能是更明智的想法”——这肯定是。有了适当的阶级责任,这样的问题就不会发生。你通常违反了拥有者的概念。任何实体都应该对自己的资源负责,完全独立于它是什么。这意味着不仅仅是内存和线程,而且绝对包括它们!CRTP如何?@Jarod42 Base dtor在派生子对象死后仍然被调用。仅仅知道调用哪个派生函数是不够的——你必须尽早调用它
class MyTaskImpl : public BackgroundTask
{
// ...
private:
std::unique_ptr<MyClass> member;
};
using MyTask = Task<MyTaskImpl>;