3

Let's say I have Foo class:

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) {}
};

Am I right and there is no guarantee that ~Foo will be called only once in such block?

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

If there is no guarantee, is it possible to fix somehow if using c++11 and move semantic? For example not call unblock_resource in moved foo, but I am not sure is there guarantee to use move constructor/operator= in return from Foo::create?

user1244932
  • 7,352
  • 5
  • 46
  • 103
  • 1
    and where do you have factory? Or where do you have a RAII? Read about this patterns first. – Marek R Dec 08 '17 at 23:16
  • Nvm, I misread. Sorry. – Baum mit Augen Dec 08 '17 at 23:16
  • @BaummitAugen I thought that there is optimization for this case, so compiler can just copy bits of result of function to allocated space on stack for `foo` in caller? – user1244932 Dec 08 '17 at 23:17
  • 1
    @MarekR Simple factory is `Foo::create`, RAII is `~Foo()` that unblock resources, I tried to make example as simple as possible – user1244932 Dec 08 '17 at 23:18
  • 2
    to have a factory you need an interface and polymorphism. To be able to use RAII you need some resource which could leak without a RAII. You do not have anything like that. – Marek R Dec 08 '17 at 23:26
  • 2
    @user1244932 I think it was changed in C++17 and is guaranteed for your case, but before that there was no such guarantee. But you can delete copy constructors and implement object moving instead - you will have to add some state like `bool blocked` member field. You question is about "copy elision". You can read more here: http://en.cppreference.com/w/cpp/language/copy_elision –  Dec 08 '17 at 23:27
  • @user1244932 Although compiler most compilers seems to be able to return object in that case without copying/moving and without even required copy/move constructor to be present. Not sure if it was changed somehow in C++11 or compiler a just a little bit non-conforming. –  Dec 08 '17 at 23:30
  • 1
    Probably your class is not movable (so you have a actual copy and dtor must be call twice) and you misunderstood your problem, so you have used some fancy words. – Marek R Dec 08 '17 at 23:33
  • @Ivan Thanks, I read your link. And updated my example code. Do I understand your link right and in `c++11` there is guarantee that my example would be compiled with copy elision? – user1244932 Dec 08 '17 at 23:33
  • 1
    @user1244932 No, there is no guarantee. I will post an answer with more information on this. –  Dec 08 '17 at 23:40

4 Answers4

7

Copy elision won't help you, since it is an optimization and may or may not be applied.

Move semantics do help, and you get guaranteed moves in function return of local variables. But that means you have to write the move constructor, and you have to modify the destructor so that it doesn't unlock the resource of an object that was moved from.

Sebastian Redl
  • 69,373
  • 8
  • 123
  • 157
  • Do you know any compiler that can help in debuging of such code? Because of `clang` and `gcc` do optimization even with `O0`, so I can not check is I do write right move constructor or not – user1244932 Dec 09 '17 at 00:35
  • @user1244932 Write code that can't elide, e.g. `Foo foo2 = std::move(foo1);` and see if it works correctly. – Sebastian Redl Dec 09 '17 at 12:16
3

Not sure about how this is related to the factory pattern, but to answer your question "Am I right and there is no guarantee that ~Foo will be called only once in such block?":

Avoiding copying/moving objects used as return values (i.e. return value optimization, especially named return value optimization) is permitted, but not guaranteed:

Under the following circumstances, the compilers are permitted, but not required to omit the copy- and move- (since C++11)construction of class objects even if the copy/move (since C++11) constructor and the destructor have observable side-effects. This is an optimization: even when it takes place and the copy-/move-constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed:

If a function returns a class type by value, and the return statement's expression is the name of a non-volatile object with automatic storage duration, which isn't a function parameter, or a catch clause parameter, and which has the same type (ignoring top-level cv-qualification) as the return type of the function, then copy/move (since C++11) is omitted. When that local object is constructed, it is constructed directly in the storage where the function's return value would otherwise be moved or copied to. This variant of copy elision is known as NRVO, "named return value optimization".

One way could be to control locking/unlocking resources in the move constructor and the destructor, just as you mentioned in your question.

Another way could be to make use of shared_ptr, such that creation and deletion of your Foo-object is managed by an RAII-style shared_ptr wrapper. There is just one tricky thing if you want to keep the Foo-constructor private, since make_shared cannot deal with private constructors. To overcome this, you could declare a public constructor taking a parameter of a private data type as parameter. A little bit ugly, and maybe a little bit clumsy due to the shared_ptr-wrapper. But maybe it's at least some inspiration:

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();
}
Stephan Lechner
  • 34,891
  • 4
  • 35
  • 58
  • I think question title should be changed to use "factory functions" instead of "factory pattern" since it confuses a lot of people. OPs code actually properly shows an example of a problem one will meet when implementing factory pattern in C++. –  Dec 09 '17 at 00:15
2

Rule of 3/5/0. You define a destructor, but not copy / move constructors / assignment operators, which is a red flag that your type is unsafe. Indeed, the destructor may get called twice prior to C++17, and it's easy to mess up while using it even post C++17.

I recommend using std::unique_ptr so that you don't define any copy/move operations or the destructor. You can use std::unique_ptr even if the resource you are managing isn't a pointer; it'd look something like this:

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) {}
};
Justin
  • 24,288
  • 12
  • 92
  • 142
1

You are looking for copy-elision.

In short, your code is guaranteed to work in C++17, as described at http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0135r0.html

In C++14 and before there is no such guarantee.

Copy elision is an optimization that existed before C++11 and allowed compiler to omit copy constructor call in some cases.

C++11 has added move semantics and copy elision was expanded to allow compiler to avoid moving or copying (if moving is not available) an object.

Regardless of what compiler actually does, your class must still provide copy or move constructor, even if one will not be used by the compiler.

C++17 introduces "guaranteed copy-elision", which allows you to return objects of non-movable classes, just like in your case. Note that proposal explicitly mentions "factory functions". Quote:

it is impossible or very difficult to write factory functions

Example from proposal has this example:

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
}

As of today, both Clang and GCC are able to compile that code with -std=c++17 flag, but not with -std=c++14.

I see two ways for you to solve this problem:

  • Use C++17: I recommend deleting both copy and move constructor (and operator=) to make sure your code will not compile under earlier standard with buggy effects.
  • Rely only on things that are available in C++14 to make your code work everywhere. You might want add additional state to your object, delete copy constructor and implement move constructor.

This is an example of how it can be done in 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};
};
  • Another question on this topic: https://stackoverflow.com/questions/38043319/how-does-guaranteed-copy-elision-work –  Dec 09 '17 at 00:21