Kyle Strand Kyle Strand - 2 months ago 15
C++ Question

Does "friending" the base class in CRTP inheritance affect the child class as well?

In an attempt to answer another question, I came up with a scheme to force children of a CRTP base class to accept a particular type as a parameter in their constructors: make the parameter type's constructor

private
, assign the CRTP base class as a
friend
, and declare the parameter type as a parameter for the base class constructor as well.

However, when I tried to demonstrate that this scheme provided the desired protections via access violations, I found that even though the parameter type's constructor was private, the child class was able to construct it:

template <typename T>
class SingletonBase {
protected: class P { friend class SingletonBase<T>; P() = default; };
public:
SingletonBase(P) {}
};

class Logger: public SingletonBase<Logger> {
using BASE = SingletonBase<Logger>;
public:
Logger() : BASE{P{}} {} // WHY NO ACCESS VIOLATION?
};


This compiles without error, even though I'd expect an access violation. Why?

Answer

Does “friending” the base class in CRTP inheritance affect the child class as well?

No, of course not. Friendship is not inherited. To illustrate the issue,

Firstly, P::P() is a defaulted default constructor, it's a trivial default constructor.

Secondly, P{} is value initialization (since C++11),

(emphasis mine)

2) if T is a class type with a default constructor that is neither user-provided nor deleted (that is, it may be a class with an implicitly-defined or defaulted default constructor), the object is zero-initialized and then it is default-initialized if it has a non-trivial default constructor;

Note it'll be only zero initialized here, not default initializated. The private default constructor of P won't be invoked at all.

If T is an non-union class type, all base classes and non-static data members are zero-initialized, and all padding is initialized to zero bits. The constructors, if any, are ignored.

If you change it to default initialization explicitly, you'll get the access violation error.

Logger() : BASE{P()} {} // error: calling a private constructor of class 'SingletonBase<Logger>::P
//               ~~

A simplified demonstration

class X { X() = default; };

int main()
{
    X x1{}; // fine
    X x2;   // error: calling a private constructor of class 'X'
}

LIVE

Solution

You can provide a user-defined default constructor, which is a non-trivial constructor, to change the behavior of value-initialization.

template <typename T>
class SingletonBase {
  protected: 
    class P { 
      friend class SingletonBase<T>; 
      P() {} // user-defined default constructor
    };
  public:
    SingletonBase(P) {} 
};

class Logger: public SingletonBase<Logger> {
  using BASE = SingletonBase<Logger>;
  public:
    Logger() : BASE{P{}} {} // error: calling a private constructor of class 'SingletonBase<Logger>::P'
};