Noncopyable and unintended ADL
This is an in-depth look at the noncopyable class in the C++ boost libraries and the unintended argument dependend lookup (ADL) protection trick.
Note: A better alternative is to delete the copy constructor and assignment, but this article is still useful to understand the mechanics of inheritance and unintended ADL.
Usage
I sometimes use boost::noncopyable
to express/ensure that a class
cannot/should not be copied. The usage is simple: derive private
from it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <boost/core/noncopyable.hpp>
class some_class :
private boost::noncopyable
{
// ...
};
void some_fn()
{
some_class a;
// line below would fail to compile
// some_class b = a;
}
Code implementation and analysis
The implementation code for boost::noncopyable
(contributed by Dave Abrahams)
looks someting like:
1
2
3
4
5
6
7
8
9
10
11
12
namespace boost {
namespace noncopyable_ {
class noncopyable {
protected:
constexpr noncopyable() = default;
~noncopyable() = default;
noncopyable(const noncopyable &) = delete;
noncopyable & operator=(const noncopyable &) = delete;
};
}
using noncopyable = noncopyable_::noncopyable;
}
Empty class
Line 3 declares the noncopyable
class to derive from. It is an empty class,
with no member variables, so in many cases deriving from it will have no size
overhead (due to the empty base class optimisation).
Prevent copy
Lines 7 and 8 delete the copy constructor and copy assignment. This prevents a derived class from being copied. It uses the C++ 11 syntax to delete them. Before C++ 11 the copy constructor and copy assignment would have been made private (and only declared, not defined).
Copy and destruction
Lines 4,5 and 6 define the default copy and destructor. They are required
because the compiler would stop generating them once we deleted the copy
function. It uses the C++ 11 syntax to request the compiler to generate
defaults. The access is protected
which allows the derived class to access
them, but prevents someone directly creating a noncopyable
instance (to
re-enforce that it’s supposed to be derived from).
The constructor is marked constexpr
. Without it one could not have a
constexpr
instance of the derived class.
Boost namespace
Line 1 and 12. Trivial.
Prevent unintended ADL
Lines 2, 10 and 11. This is for me the most interesting feature of the implementation. The potential problem is caused by ADL.
ADL (of Andrew Koenig fame) allows the code below to work, because in main
,
the function some_fn
is not qualified with a namespace, so some_fn
is
looked up (and found) in the namespace X
for the type Y
of it’s argument
z
.
1
2
3
4
5
6
7
8
9
namespace X {
struct Y {};
void some_fn(const Y &) {}
}
int main() {
X::Y z;
some_fn(z);
}
Often that’s what we want, and it’s what makes operator<<
find the right
namespace, based on the arguments provided.
However, the trick is that the lookup is also performed in the namespace of the
derived classes for the argument. We would not want a class derived from
noncopyable
to get involved in ADL against functions in the boost
namespace.
To avoid that, noncopyable
is not declared in the boost
namespace directly,
but instead in a namespace called noncopyable_
(notice the extra ending
underscore) which has no functions, hence avoids the unintended ADL. The name
noncopyable
is then reintroduced to the boost
namespace with the using
on
line 11.
Controversies
Taste and style
Which is better? Should one use noncopyable
:
1
2
3
4
5
#include <boost/core/noncopyable.hpp>
struct one_class:
private boost::noncopyable
{
};
or explicitly delete copy constructor an assignment:
1
2
3
4
5
struct two_class
{
two_class(const two_class &) = delete;
two_class & operator=(const two_class &) = delete;
};
or rely on side effects of implementing move:
1
2
3
4
5
struct three_class
{
three_class(three_class &&) = default;
three_class & operator=(three_class &&) = default;
};
I think people could easily argue either way.
My view is that not all classes are equal. Some are used very few times (e.g. once or twice), others are library classes used a lot.
For library classes, especially if I need to put the effort to implement move
as well, then I’ll be explicit about deleting the copy operations, not use
noncopyable
and not rely on side effects.
But for classes that are used just a few time in my code, I find that the
noncopyable
approach is easier to read, because it avoids the repetition of
the class name.
Optimisation issues
Empty classes still have a sizeof
equal to 1, the idea being that it is
required so that we can get the address of an instance of that class. The empty
class optimisation ensures that an empty base class does not add to the derived
class size.
However in a diamond derivation case like below there is an overhead for
deriving from the same noncopyable
class (compared with just deleting copy
constructor and assignment for classes A
and B
.
1
2
3
4
5
6
7
8
9
10
#include <iostream>
struct A: private boost::noncopyable { };
struct B: private boost::noncopyable { };
struct C: public A, public B { };
int main() {
// prints 2
std::cout << sizeof(C) << "\n";
}
The size is 2 in the example above so that there are different addresses for
each noncopyable
base class.
However in practice this overhead might not be an issue, because if for example
class C
would have a member of type int
of size 4, then the size of C
would be just 4, not 6.