1

Approach 1

I have some type Foo which internally contains a std::mutex.

class Foo {
  std::mutex m_;
};

I wrote another class Bar. Bar has Foo as a member and a constructor, like this (note - this code doesn't compile):

class Bar {
  Bar(Foo foo) : foo(foo) {}
...
private:
  Foo foo;
};

My IDE complains:

Call to implicitly-deleted copy constructor of Foo. copy constructor of 'Foo' is implicitly deleted because field 'm_' has a deleted copy constructor

Approach 2

So then I tried to do an assignment like this:

class Bar {
  Bar(Foo fooIn) { foo = fooIn; }
...
private:
  Foo foo;
};

But this also says:

Object of type 'Foo' cannot be assigned because its copy assignment operator is implicitly deleted

Approach 3 (using pointer)

The only way I can get this to work is like this:

class Foo {
private:
    std::mutex m_;
};

class Bar {
    Bar(std::unique_ptr<Foo> fooIn) : foo(std::move(fooIn)) {}
private:
    std::unique_ptr<Foo> foo;
};

I understand the smart pointer helps with memory management. But lets put that feature aside.

In my examples above, the main difference between not using the smart pointer and using the smart pointer is that the smart pointer has indirection – it's pointing to an object on the heap. Right?

What is the right mental model to understand this? Is it that because Foo has a deleted copy constructor and deleted copy assignment, my only option is to have it constructed on the heap and then work via indirection (via pointers)?

Is that correct? Or am I completely off?

Beebunny
  • 4,190
  • 4
  • 24
  • 37

2 Answers2

1

Is it that because Foo has a deleted copy constructor and deleted copy assignment, my only option is to have it constructed on the heap and then work via indirection (via pointers)?

Close, but not quite.

Because a mutex isn't copyable, Foo isn't copyable either by default. You're trying to copy, and that's not possible.

Using indirection to refer to a Foo stored elsewhere is possible. But it isn't necessary for that Foo to have dynamic storage to achieve that. Nor does the non-copyability imply the need for indirection... unless you want to prevent the non-copyability from propagating to the enclosing class.

The only constructor a mutex has is the default constructor. Thus, what you can reasonably do is simply default initialise Foo. A minimal example:

struct Bar {
    Foo foo;
    // no need to declare the default constructor
    // it is generated implicitly
};

Bar bar; // this just works
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Aha! Of course, this makes sense. May I ask a follow up question? What if I want to write a unit test for Bar, and for this unit test, I want to provide Bar a foo "mock" instance, as opposed to have it generate implicitly? – Beebunny Jun 18 '21 at 06:51
  • @Beebunny Mocking doesn't work with object members. You need some form polymorphism. Either static polymorphism by making `Bar` a template and `Foo` a template type parameter, or dynamic polymorphism using indirection and either inheritance or type erasure. – eerorika Jun 18 '21 at 07:02
1

If you check how mutex is implemented, you will see that its copy constructor and copy assignment operator are deleted, but the move constructor and move assignment operator are not, that's why your smart pointer example works.

my only option is to have it constructed on the heap and then work via indirection (via pointers)?

No, that is not the only way of doing it. If you slightly change your first example to use references, that will work as well (and foo is not constructed on heap):

class Foo {
    std::mutex m_;
};

class Bar {
    Bar(Foo& foo) : foo(foo) {}

private:
    Foo& foo;
};
Zoltán
  • 678
  • 4
  • 15
  • What I want to do is vend a library, and Bar is the public facing API. If Bar is the "root" of my dependency graph, I don't think it makes sense for Bar to take a "reference" to Foo. Bar needs to "own" Foo – not necessarily using a pointer, but there's only one Foo and it belongs to Bar. If I give Bar a reference to Foo, who would own Foo and ensure it doesn't get deallocated? – Beebunny Jun 18 '21 at 06:51
  • Hmm. Unlike pointers, you can assign a temporary variable to a `const` reference and this temporary object won't be destroyed during the lifetime of the reference variable. Now, in Visual Studio you can assign a temporary to a non-const reference as well, and I guess this temporary foo object will live while the foo reference lives. BUT it is not a standard way. So, for example if you do this: `Bar(Foo());` will work in MSVC, but it fails to compile with GCC. – Zoltán Jun 18 '21 at 08:31