从成员函数重构类

时间:2018-12-03 19:19:12

标签: c++

我在std :: thread周围有一个小的包装,如果可能,它会在生命周期尽头自动加入,看起来像这样(实际的包装具有更多功能,但该线程只是该问题的一个示例):

#include <thread>

struct my_thread : std::thread
{
    // Default Constructor
    my_thread() = default;

    // Forwards anything to thread
    template<typename... Ts>
    my_thread(Ts&&... ts)
    :
        std::thread( std::forward<Ts>(ts)... )
    {}

    // Destructor
    ~my_thread()
    {
        if ( joinable() ) { join(); }
    }
};

然后我想添加一个重新启动成员函数,该成员函数连接当前线程(如果有的话)并创建一个新线程。我遇到的最简单的方法是:

// Restarts the thread
template<typename... Ts>
void restart(Ts&&... ts)
{
    // Destruct current thread
    this->~my_thread();

    // Create new thread on this object
    new (this) my_thread ( std::forward<Ts>(ts)... );
}

但是我不确定这是否是个好习惯,甚至可能失败。 我认为失败的原因是:

  • (1)如果调用析构函数时抛出该异常,则在取消堆栈堆栈时,将在销毁局部变量时再次调用该析构函数。

  • (2)如果调用构造函数时抛出任何异常,则该对象将处于未完成状态,并且当局部变量被破坏时,将在一个未完成的对象上调用该析构函数,这是未定义的行为

由于这两个问题都涉及异常,所以我认为我可以添加一条try / catch all语句,但是我不确定在失败时该怎么做。

例如,如果析构函数成功但构造函数失败,我该怎么办?

我不能让对象处于不完整的状态,因为当调用局部变量的析构函数时,它将是未定义的行为,因此我想到了只做无限循环,直到构造函数成功,但这听起来并不合理。就像一个好主意。

我应该将当前对象保存在某个地方,如果其中任何一个失败,则像这样还原吗?

// Restarts the thread
template<typename... Ts>
void restart(Ts&&... ts)
{
    // Copy
    char backup[ sizeof( my_thread ) ];
    memcpy(backup, this, sizeof(my_thread) );

    try
    {
        // Destruct current thread
        this->~my_thread();

        // Create new thread on this object
        new (this) my_thread ( std::forward<Ts>(ts)... );
    }
    catch (...)
    {
        // Copy backup
        memcpy(this, backup, sizeof(my_thread));
    }
}

这感觉像一个可怕的主意,因为我不仅在没有例外的情况下创建不必要的副本,而且还忽略了构造函数/析构函数的任何副作用,因此我认为这不是一个好答案。

我还考虑过像这样使用std::thread的move构造函数:

// Restarts the thread
template<typename... Ts>
void restart(Ts&&... ts)
{
    static_cast<std::thread&>(*this) = std::move( std::thread{ std::forward<Ts>(ts)... } );
}

这似乎可行,但是我不确定这是一个陷阱。而且我认为一般来说,顶层代码更有用,因为它也适用于不可移动或不可复制的类型,所以我想知道最好的方法是什么。

具体来说,我的问题是“重构”这样的对象是否是一个好主意,并且在执行此操作时是否应考虑任何事项?

这也可以应用于任何事物,我只是使用线程,因为这是问题首先出现的地方。

编辑

这是我希望最终产品执行的操作的一个示例,但是,如果可能的话,请不要停止从std :: thread(或任何类型)继承:

struct my_thread
{
    // The thread memory
    char thread_mem[ sizeof(std::thread) ];

    bool thread_constructed = false;

    // Default Constructor
    my_thread() = default;

    // Forwards anything to thread
    template<typename... Ts>
    my_thread(Ts&&... ts)
    {
        new ( thread_mem ) std::thread ( std::forward<Ts>(ts)... );
        thread_constructed = true;
    }

    // Destructor
    ~my_thread()
    {
        // Call destructor if it's constructed
        if ( thread_constructed )
        {
            if ( reinterpret_cast<std::thread*>(thread_mem)->joinable() )
            {
                reinterpret_cast<std::thread*>(thread_mem)->join();
            }

            thread_constructed = false;
            reinterpret_cast<std::thread*>(thread_mem)->~thread();
        }
    }

    // Restarts the thread
    template<typename... Ts>
    void restart(Ts&&... ts)
    {
        // Destruct current thread
        this->~my_thread();

        // Create new thread on this object
        new (this) my_thread ( std::forward<Ts>(ts)... );
    }
};

编辑2

我希望有可能使100%的泛型类型能够处理此问题,我相信双重析构函数会从以前调用问题以及被破坏的不完整对象:

template<typename T>
struct restartable_type
{
    // Memory
    alignas(T) char mem[ sizeof(T) ];

    bool constructed = false;

    template<typename... Ts>
    restartable_type(Ts&&... ts)
    {
        new ( mem ) T ( std::forward<Ts>(ts)... );
        constructed = true;
    }

    ~restartable_type()
    {
        reinterpret_cast<T*>(mem)->~T();
        constructed = false;
    }

    template<typename... Ts>
    void restart(Ts&&... ts)
    {
        this->~restartable_type();

        new ( this ) restartable_type<T> ( std::forward<Ts>(ts)... );
    }
};

我要改进的唯一一件事就是从T继承,这样我就可以在restartable_type上从T调用成员函数,但是我不知道有什么方法可以解决此析构函数问题,我能想到的就是重载operator->来重定向到T,但这只是解决方案的一半,因为我更喜欢使用。 (点),我只是不知道如何解决析构函数的问题,这就是我想要的帮助。如果我不清楚这个问题,我深表歉意。

2 个答案:

答案 0 :(得分:1)

如果您的主构造函数抛出以下错误,而您可以退回到noexcept默认构造函数,则可以使此想法起作用:{p}

template<typename... Ts>
void restart(Ts&&... ts)
{
    this->~my_thread();

    try {
        new (this) my_thread(std::forward<Ts>(ts)...);
    } catch (...) {
        new (this) my_thread();
        throw;
    }
}

您甚至可以通过检查is_nothrow_default_constructible<std::thread>::value来启用它,这是正确的。

但是,在执行此操作时可能需要考虑太多。例如,没有人可以进一步继承您的课程。或者,您可能隐式地公开了一些接口的方法,可以在重建时打破类协定。使用std::thread时,它们分别是get_id()native_handle()-但是,它们在分配时也被破坏了,因此,如果您的用户知道 {{1 }}是一项作业。更糟糕的是,当用户先呼叫restart()然后呼叫detach()时会产生歧义。它会起作用,但在用户可能期望的意义上来说并非如此。

还有另一件事要考虑:您不会通过加入线程来停止线程。加入只是等待线程完成。您需要一些其他的信号通知机制来告知线程完成,然后使用这种包装程序并不会带来什么好处。

答案 1 :(得分:0)

有一些通用的解决方法,每种方法都有其起伏:

1-std::optional<T>

通过拥有std::optional<T>的成员,您可以像这样围绕T提供一个瘦的拥有包装器:

#include <optional>

template<typename T>
struct reset_type
{
    // The value itself
    std::optional<T> value;

    // Constructs value in-place with arguments
    template<typename... Ts>
    constexpr reset_type(Ts&&... ts)
    :
        value(std::in_place_t{}, std::forward<Ts>(ts)... )
    {}

    // Resets the type
    template<typename... Ts>
    constexpr void reset(Ts&&... ts)
    {
        value.reset();
        value.emplace( std::forward<Ts>(ts)... );
    }

    // Conversion operator
    constexpr operator       T&()       { return *value; }
    constexpr operator const T&() const { return *value; }

    // Accesses T
    constexpr       T* operator->()       { return &(*value); }
    constexpr const T* operator->() const { return &(*value); }
};

这种方法很可能是实现此包装的最简单,最好的方法,您唯一要放弃的就是通过来访问成员。 (点)运算符。

2-继承

我们可以从T继承并以这种方式构造它:

template<typename T>
struct reset_type : T
{
    // Constructs value in-place with arguments
    template<typename... Ts>
    constexpr reset_type(Ts&&... ts)
    :
        T( std::forward<Ts>(ts)... )
    {}


    // Resets the type
    template<typename... Ts>
    constexpr void reset(Ts&&... ts)
    {
        this->~reset_type();

        new ( this ) reset_type<T> ( std::forward<Ts>(ts)... );
    }
};

这个问题出在构造函数或析构函数抛出异常时,我不确定它的细节是什么,但是除非它们抛出异常,否则不会发生任何坏事

这样做的好处是您可以使用静态强制转换将其转换为T,因为它继承自它,并且可以使用来访问T。 (点)运算符,使它变得更加有用。

T替换几个std::remove_cv<T>也是值得的。

如果还有其他解决方案,请告诉我,我将在此处添加。