In the code below, class B
contains an array of member class class A
.
B::A
has one member bool
and one member std::thread
.
The code below compiles fine:
// main.cpp
#include <mutex>
#include <thread>
class B {
public:
B();
private:
class A {
public:
A( const bool& b ) : b_( b ) {}
bool b_;
std::thread thread_;
} a_[2];
};
B::B() : a_{ { false }, { false } } { }
int main( int argc, char* argv[] ) {
B b;
return 0;
}
$ g++ --version && g++ -g ./main.cpp
g++ (Debian 6.3.0-18+deb9u1) 6.3.0 20170516
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$
Why does introducing a std::mutex to B::A
introduce the following compile error?
// main.cpp
#include <mutex>
#include <thread>
class B {
public:
B();
private:
class A {
public:
A( const bool& b ) : b_( b ) {}
bool b_;
std::mutex mutex_; // I break compilation!
std::thread thread_;
} a_[2];
};
B::B() : a_{ { false }, { false } } { }
int main( int argc, char* argv[] ) {
B b;
return 0;
}
$ g++ -g ./main.cpp
./main.cpp: In constructor ‘B::B()’:
./main.cpp:21:35: error: use of deleted function ‘B::A::A(B::A&&)’
B::B() : a_{ { false }, { false } } { }
^
./main.cpp:11:9: note: ‘B::A::A(B::A&&)’ is implicitly deleted because the default definition would be ill-formed:
class A {
^
./main.cpp:11:9: error: use of deleted function ‘std::mutex::mutex(const std::mutex&)’
In file included from /usr/include/c++/6/mutex:44:0,
from ./main.cpp:2:
/usr/include/c++/6/bits/std_mutex.h:97:5: note: declared here
mutex(const mutex&) = delete;
^~~~~
If I correctly understand the compile error, it's complaining that an instance of B::A
cannot be created without explicit construction of B::A::mutex_
. But if this is true, I don't understand why this should be necessary: std::mutex
has a default constructor, so doesn't need any constructor arguments, as demonstrated below:
// main.cpp
#include <mutex>
int main( int argc, char* argv[] ) {
std::mutex mutex[10];
return 0;
}
Please help me understand the nature of the above compile error, and what an appropriate fix might be.
Update: @Jarod42 and @chris seem to have discovered this is a compiler bug. I'm updating the question to ask if anyone could explain the nature of this bug -- initiaizing member array-of-object elements seems like such a simple and foundational thing. What type of objects trigger this bug and why? I can't imagine this could be a universal/easily reproducible problem...?
Update: A not-great workaround seems to be making B::A::A
an empty constructor and initializing B::A::b_
with an rvalue. :(
// main.cpp
#include <mutex>
#include <thread>
class B {
public:
B();
private:
class A {
public:
A() : b_( false ) {}
bool b_;
std::mutex mutex_;
std::thread thread_;
} a_[2];
};
B::B() { }
int main( int argc, char* argv[] ) {
B b;
return 0;
}
$ g++ -g ./main.cpp
$
The apparent, likely cause of the bug is a subtle difference between copy-initialization and copy-list-initialization:
struct A {
A(int);
A(A&&)=delete;
} a=1, // error: not movable
b=A(1), // error
c={1}, // OK, no temporary constructed
d[]={1}, // error
e[]={A{1}}, // error
f[]={{1}}; // OK (the compiler bug)
Here a bare 1
is converted to a temporary A
which cannot be copied/moved, whereas {1}
is used to initialize the ultimate A
despite the term “copy”. This distinction vanishes in C++17, where initialization from a prvalue (as for b
or a
after conversion) invokes only the prvalue’s constructor (“mandatory copy elision”).
The issue also couldn’t arise prior to C++11, since non-static array members could only be default- or value-initialized. (={}
was also considered normal copy-initialization then and didn’t apply to class objects at all.) This is why your workaround worked, and the gradual adoption of the new initializers is probably why the compiler bug lasted as long as it did.
Note that, despite the error mentioning std::mutex
’s deleted copy constructor, it is its non-movability that matters (as indicated by the B::A::A(B::A&&)
), which is how it differs from std::thread
.