RAII和工厂设计模式?

时间:2017-12-08 23:13:26

标签: c++ c++11 factory c++17 copy-elision

我们说我有Foo课程:

struct Resource {
  void block();
  void unblock();
};
struct Foo {
   static Foo create() {
      Resource resource;
      resource.block();
      return Foo{resource};
    }
   ~Foo() { resource.unblock(); }
   void f() {}
private:
   Resource resource;
   Foo(Resource resource): resource(resource) {}
};

我是对的,并且无法保证~Foo只会在此类块中被调用一次吗?

{
   Foo foo = Foo::create();
   foo.f();
}

如果没有保证,是否有可能以某种方式修复如果使用c ++ 11并移动语义?例如,不要在移动的foo中调用unblock_resource,但我不确定是否保证使用移动构造函数/ operator =从Foo::create返回?

4 个答案:

答案 0 :(得分:4)

复制elision对您没有帮助,因为它是一种优化,可能会也可能不会应用。

移动语义确实有帮助,并且您可以在函数返回局部变量时得到保证。但这意味着必须编写移动构造函数,必须修改析构函数,以便它不会解锁被移动的对象的资源。 / p>

答案 1 :(得分:2)

不确定这与工厂模式有什么关系,但要回答你的问题“我是否正确,并且无法保证在这样的阻止中只会调用~Foo一次?”:

允许复制/移动用作返回值的对象(即返回值优化,特别是命名返回值优化),但不能保证:

  

在以下情况下,允许使用编译器,但是   不需要省略复制和移动 - (自C ++ 11)构造   类对象即使复制/移动(自C ++ 11)构造函数和   析构函数具有可观察到的副作用。这是一个优化:甚至   当它发生并且没有调用copy- / move-构造函数时,它   仍然必须存在且可访问(好像没有优化发生   在所有情况下),否则该程序是不正确的:

     

如果函数按值返回类类型,则返回   statement的表达式是一个非易失性对象的名称   自动存储持续时间,不是函数参数,或者是   catch子句参数,它具有相同的类型(忽略   顶级cv-qualification)作为函数的返回类型,然后   复制/移动(因为C ++ 11)被省略。当那个本地对象是   建造后,它直接在仓库中建造   否则将移动或复制函数的返回值。这个   复制elision的变体被称为NRVO,“命名返回值   优化”。

一种方法是控制移动构造函数和析构函数中的锁定/解锁资源,就像您在问题中提到的那样。

另一种方法是使用shared_ptr,以便创建和删除Foo - 对象由RAII样式的shared_ptr包装器管理。如果你想让Foo - 构造函数保持私有,那么只有一件棘手的事情,因为make_shared无法处理私有构造函数。为了解决这个问题,您可以声明一个公共构造函数,将私有数据类型的参数作为参数。由于shared_ptr - 包装器,有点难看,可能有点笨拙。但也许这至少是一些灵感:

struct Foo {
private:

    struct private_dummy {};

public:
    static shared_ptr<Foo> create() {
        shared_ptr<Foo> foo = make_shared<Foo>(private_dummy{});
        return foo;
    }
    ~Foo() { cout << "deleted."; }
    Foo(struct private_dummy x) { cout << "created."; }
};

void test() {

    shared_ptr<Foo> foo = Foo::create();

}

int main() {

    test();

    //Foo notOK();
}

答案 2 :(得分:1)

您正在寻找copy-elision

简而言之,您的代码可以保证在C ++ 17中工作,如http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0135r0.html

所述

在C ++ 14中,之前没有这样的保证。

复制elision是一种在C ++ 11之前存在的优化,并允许编译器在某些情况下省略复制构造函数调用。

C ++ 11添加了移动语义,并且扩展了复制省略以允许编译器避免移动或复制(如果移动不可用)对象。

无论编译器实际执行什么操作,您的类仍必须提供复制或移动构造函数,即使编译器不会使用它。

C ++ 17引入了“保证copy-elision”,它允许您返回不可移动类的对象,就像您的情况一样。请注意,提案明确提到“工厂功能”。引用:

  

编写工厂函数是不可能或非常困难的

提案中的示例有此示例:

struct NonMoveable {
  NonMoveable(int);
  NonMoveable(NonMoveable&) = delete;
  void NonMoveable(NonMoveable&) = delete;
  std::array<int, 1024> arr;
};
NonMoveable make() {
  return NonMoveable(42); // ok, directly constructs returned object
}

截至今天,Clang和GCC都能够使用-std=c++17标志编译该代码,但不能使用-std=c++14编译。

我认为有两种方法可以解决这个问题:

  • 使用C ++ 17:我建议删除复制和移动构造函数(以及operator=)以确保您的代码不会在早期标准下编译并带有错误效果。
  • 仅依靠C ++ 14中提供的功能,使您的代码无处不在。您可能希望向对象添加其他状态,删除复制构造函数并实现移动构造函数。

这是如何在C ++ 14中完成的一个例子。

class Foo {
public:
    Foo() = default;
    Foo(const Foo &) = delete;
    Foo(Foo &&rvalue) noexcept { std::swap(blocked, rvalue.blocked); }
    ~Foo() { if (blocked) unblock();
    void block() { blocked = true; }
    void unblock() { blocked = false; }
private:
    bool blocked{false};
};

答案 3 :(得分:1)

3/5/0的规则。您可以定义析构函数,但不能复制/移动构造函数/赋值运算符,这是一个类型不安全的红色标记。实际上,析构函数可能在C ++ 17之前被调用两次,并且即使在C ++ 17之后使用它也很容易搞乱。

我建议使用std::unique_ptr,这样就不会定义任何复制/移动操作或析构函数。即使您管理的资源不是指针,也可以使用std::unique_ptr;它看起来像这样:

class Resource {
    int handle;

public:
    Resource(std::nullptr_t = nullptr)
        : handle{}
    {}
    Resource(int handle)
        : handle{ handle }
    {}

    explicit operator bool() const { return handle != 0; }
    friend bool operator==(Resource lhs, Resource rhs) { return lhs.handle == rhs.handle; }
    friend bool operator!=(Resource lhs, Resource rhs) { return !(lhs == rhs); }

    void block() { std::cout << "block\n"; }
    void unblock() { std::cout << "unblock\n"; }

    struct Deleter {
        using pointer = Resource;

        void operator()(Resource resource) const {
            resource.unblock();
        }
    };
};

struct Foo {
    static Foo create() {
        Resource resource{42};
        resource.block();
        return Foo{resource};
    }
    void f() {}
private:
    std::unique_ptr<Resource, Resource::Deleter> resource;
    Foo(Resource resource): resource(resource) {}
};