Search code examples
c++inheritanceprivatefriendcrtp

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?


Solution

  • 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'
    };