Some notes on computer stuff

C++11+ aggregate initializer surprise

October 24, 2015
[c++] [c++11] [programming] [initialization]

There is a nice thing in C++ called friendship between classes or classes and functions, which is used to put several entities in one security domain by allowing "friends" to access private fields. This language feature is usually criticized as something that breaks encapsulation, which is not actually true as C++ is multi-paradigm language rather than being just OOP-language and such exceptions should be viewed as another way of doing things, not as something "wrong" (which is very subjective, by the way).

That said, sometimes it's useful to obtain finer control over which classes what methods/fields can access. In this case there are at least Attorney-Client and Passkey idioms. The first one seems to be particularly useful for libraries (e.g. it's used by Boost.Iterator) or similar things where you have many-to-one kind of relationships (I also use it in tests). The latter one is nice when you want to enable another class to access a couple of methods. Thanks to new syntactic sugar available in C++ since C++11 this one implies very little additional code, but there is a caveat...

Implementation

One should go and read that post on Passkey as I'm not going to repeat what's said there. Instead we go straight to my initial implementation which looks like this and does not allow object to be copied or moved (to forbid "cheating" and reusing of the same "passkey" more than once):

template <typename T>
class Passkey
{
    friend T;

private:
    Passkey() = delete;

public:
    Passkey(const Passkey &rhs) = delete;
    Passkey & operator=(const Passkey &rhs) = delete;
};

template <typename T>
using pk = Passkey<T>;

Here is usage example:

class Item
{
public:
    Item(pk<Storage>);
    // ...
};

Storage::Storage()
{
    Item item({});
}
// ...

Seems legit? It works indeed, but the question is when it works.

Test

Here is a small test to get reader acquainted with the problem:

#include <initializer_list>

class Class
{
private:
    Class() = delete;
    Class(Class &) = delete;
    Class(Class &&) = delete;
    Class(const Class &&) = delete;
    Class(std::initializer_list<int>) = delete;
    Class & operator=(Class &rhs) = delete;

    Class(const Class &rhs) = delete;
    Class & operator=(const Class &rhs) = delete;
};

void
func(Class pk)
{
}

int
main(void)
{
    Class c0;      // 1
    Class c1{};    // 2
    Class c2 = {}; // 3
    func({});      // 4
    return 0;
}

Which of the lines marked from 1 to 4 do you expect to compile without errors?

Give it a thought.

What's the hell?

Mine answer was that none of them should compile because everything is marked as deleted, but here's what standard has to say:

[dcl.init.aggr]
An aggregate is an array or a class with no user-provided constructors, no
private or protected non-static data members, no base classes, and no virtual
functions.

Were you able to notice the caveat? Here it is:

... user-provided ...

defaulted or deleted constructors or operators are not counted as being "user-provided", hence Class is an aggregate (a POD) (use std::is_pod to check yourself), and aggregate initialization is allowed.

So, here goes the correct answer: only #1 fails to compile drawing Passkey rather useless.

Solution

Luckily, it's easy to fix with:

-    Passkey() = delete;
+    Passkey() {}

Now user-provided constructor makes all four cases fail to compile as desired. Alternatively one could define a private data member to make type non-POD, but empty function body will do it for me.