C++11 案例研究:如何使用智能指针实现工厂设计? [示例与测试]

C++11 case study: How to implement factory design with smartpointers? [Example and test]

这是一个在 C++11 中使用工厂设计模式的案例研究。如果有人能解释修复尝试中实际发生的事情,我将很高兴:

基本设计

假设我们想从工厂获取我们不想显式保存的元素,但我们想确保它们被正确删除。我用谷歌搜索了示例,但我似乎没有找到任何示例,所以我们需要做一些测试:

我认为我们可以实现目标的方法是使用 std::unique_ptr 对象来处理所有删除任务。我在这里只喜欢 unique_ptr 而不是 shared_ptr,因为它会在离开范围时调用持有元素的析构函数,因此使用它会使任何隐藏的复制可见。首先让我们创建一个能够创建 lambda 函数的简单工厂:

class To_do_base
{
    public:
        std :: function<void()> what_to_do;
        To_do_base() {};
        virtual ~To_do_base() { std :: cout << "~To_do_base()..." << std :: endl; };
        std :: function<void()>& get_function() { return this -> what_to_do; };
};


class Special_to_do : public To_do_base
{
    public:
        Special_to_do()
        { 
            this -> what_to_do = []()
            {
                std :: cout << "I have a special thing for you to do!" << std :: endl;
            };
        };
        ~Special_to_do() { std :: cout << "~Special_to_do()..." << std :: endl; };
};

所以现在每当我们调用 Special_to_do() 构造函数时,我们都会生成一个函数来打印 "I have a special thing for you to do!". 现在让我们创建一个工厂来为我们实例化这些 类:

class To_do_factory
{
    public:
        static std :: unique_ptr<To_do_base> get_to_do( const std :: string& keyword_p )
        {
            if( keyword_p == "special" )
            {
                return std :: unique_ptr<To_do_base>( new Special_to_do() );
            }
            std :: cerr << "Error: Argument_factory::get_argument()..." << std :: endl;
            return nullptr;
        };
};

现在让我们创建一个 main() 并祈祷吧!

int main()
{
    To_do_factory :: get_to_do( "special" ) -> get_function()();
    return 0;
}

程序输出:

I have a special thing for you to do!
~Special_to_do()...
~To_do_base()...

问题

到目前为止一切顺利。但是让我们通过先保存函数然后再调用它来仔细检查结果:

int main()
{
    std :: function<void()> to_do = To_do_factory :: get_to_do( "special" ) -> get_function();
    to_do();
    return 0;
}

程序输出:

~Special_to_do()...
~To_do_base()...
I have a special thing for you to do!

所以看起来return值现在实际上是最初创建的函数的副本。为什么这很糟糕?通常,当您传递 lambda 函数时,它们会捕获其范围内的一些变量(例如 this)。但是,在复制它们时,这些变量可能会超出范围,从而导致不太严重的分段错误。


我有一个答案,其中我写下了我到目前为止推断的所有内容,以及 "hack" 这个设计的方法。但我不知道我在那里做了什么,这只是一个案例研究。请帮助我了解那里出了什么问题。谢谢!

正在尝试解决问题

测试 get_function() return 值

可能不是智能指针的问题,而是get_function()的return值。我们应该测试一下:

int main()
{
    To_do_base* a = new Special_to_do();
    std :: function<void()> to_do = a -> get_function();
    to_do();
    delete a;
    return 0;
}

这个程序输出:

I have a special thing for you to do!
~Special_to_do()...
~To_do_base()...

这是正确的行为。

试图通过移动 unique_ptr 来强行通过它

也许我们应该以某种方式尝试强制工厂 return 原始对象。也许通过对 return 值执行 std::move() 来 returning 一个右值可以帮助:

class To_do_factory
{
    public:
        static std :: unique_ptr<To_do_base>&& get_to_do( const std :: string& keyword_p )
        {
            if( keyword_p == "special" )
            {
                return std :: move( std :: unique_ptr<To_do_base>( new Special_to_do() ) );
            }
            std :: cerr << "Error: Argument_factory::get_argument()..." << std :: endl;
            exit( -1 );
            return std :: move( std :: unique_ptr<To_do_base>( nullptr ) );
        };
};

代码编译,输出为:

~Special_to_do()...
~To_do_base()...
[1]    329 segmentation fault  ./main

嗯,这有点尴尬......如果我们先保存右值然后调用它呢?检查后:不传递右值引用就好了,但它仍然会用右值创建分段错误。

在 main()

中保存 unique_ptr

所以让我们回到左值。此代码与工厂的第一种方法:

int main()
{
    std :: unique_ptr<To_do_base> saved_return_value( To_do_factory :: get_to_do( "special" ) );
    std :: cout << "Return value saved." << std :: endl;
    //std :: function<void()> to_do = To_do_factory :: get_to_do( "special" ) -> get_function(); // WRONG: assigns by copying OR frees dinamically allocated memory after assigning
    std :: function<void()> to_do = saved_return_value -> get_function();
    to_do();
    return 0;
}

生成良好的输出:

Return value saved.
I have a special thing for you to do!
~Special_to_do()...
~To_do_base()...

好吧,它工作得很好......但是它有点失去了拥有功能工厂的意义。在这里我们能够生成工厂元素,但只有在我们保存它们时...

在工厂中保存指针

也许有一种方法可以在工厂中保存指针。我们应该尝试在工厂中创建一个静态成员变量来处理保存。 std :: vector 应该没问题。

class To_do_factory
{
    public:
        static std :: vector<std :: unique_ptr<To_do_base>> to_do_list;
        static std :: unique_ptr<To_do_base>& get_to_do( const std :: string& keyword_p )
        {
            if( keyword_p == "special" )
            {
                to_do_list.push_back( std :: unique_ptr<To_do_base>( new Special_to_do ) );
                return to_do_list.back();
                //return std :: unique_ptr<To_do_base>( new Special_to_do() );
            }
            std :: cerr << "Error: Argument_factory::get_argument()..." << std :: endl;
            exit( -1 );
            return to_do_list.back();
        };
};

std :: vector<std :: unique_ptr<To_do_base>> To_do_factory :: to_do_list; 
// global scope I have no idea, where else could I define this...

输入是:

I have a special thing for you to do!
~Special_to_do()...
~To_do_base()...

终于可以输出了! :)

测试

我们不要太过分了,我们应该测试 lambda returned 是否真的是我们想要的。我们可以尝试给它一个值来捕获:

class Special_to_do : public To_do_base
{
    private:
        int x = 2;
    public:
        Special_to_do()
        { 
            this -> what_to_do = [&]()
            {
                std :: cout << "I have " << this -> x << " special thing for you to do!" << std :: endl;
            };
        };
        ~Special_to_do() { std :: cout << "~Special_to_do()..." << std :: endl; };
};

输出:

I have 2 special thing for you to do!
~Special_to_do()...
~To_do_base()...

这是预期的输出。我仍然对这种行为感到困惑......任何人都可以告诉我 "more correct" 实现这个的方法吗? 谢谢:)

如果我理解,问题是如果存储、复制等,如何防止 lambda 捕获的变量超出范围(已经被破坏)

这里有两个建议。

Lambda 经常回调。在超出范围之前,对象从将回调它的服务中注销自己。

或者,lambda 捕获的所有指针(其生命周期显然不安全)应该是 shared_ptrs。如果在调用 lambda 时对象确实始终可用,那么这很好。您可能会发现 http://en.cppreference.com/w/cpp/memory/enable_shared_from_this 有帮助的讨论。