1

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?

Community
  • 1
  • 1
Kyle Strand
  • 15,941
  • 8
  • 72
  • 167

2 Answers2

2

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'
};
songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • So in short, it sounds like using `=default` provides an access-specifier loophole for the "private constructor" trick. Would switching from `=default` to `{}` *ensure* that the class *cannot* be constructed from scratch by non-friends? – Kyle Strand Sep 10 '16 at 14:59
  • @KyleStrand Yes. The point is user-defined constructor is non-trivial, which will change the behavior of value-initialization. – songyuanyao Sep 10 '16 at 15:55
  • If you add that as a solution (since the goal is to prevent non-`friend` classes from creating instances of the object), I'll accept the answer. – Kyle Strand Sep 10 '16 at 16:15
  • @KyleStrand Sure, added. – songyuanyao Sep 11 '16 at 03:05
0

What you have done has nothing to do with your friend statement!

If you remove your friend the code compiles also fine!

That is because a default constructor for an empty class is public:

From C++11 standard:

If there is no user-declared constructor for class X, a constructor having no parameters is implicitly declared as defaulted. An implicitly-declared default constructor is an inline public member of its class.

If you have no default constructor like this:

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

    public:
        SingletonBase(P) {}
};

class Logger: public SingletonBase<Logger>
{
    using BASE = SingletonBase<Logger>;

    public:
    Logger() : BASE(P{1}) {} // WHY NO ACCESS VIOLATION?
};

You will get the "access" violation and you see that your friend did not work!:

main.cpp: In constructor 'Logger::Logger()':
main.cpp:10:17: error: 'SingletonBase<T>::P::P(int) [with T = Logger]' is private
                 P(int){ }
                 ^
main.cpp:22:28: error: within this context
         Logger() : BASE(P{1}) {} // WHY NO ACCESS VIOLATION?
Klaus
  • 24,205
  • 7
  • 58
  • 113
  • ...but explicitly declaring the constructor `default` **is** declaring a constructor. I thought that didn't count as "no user-declared constructor"; in fact, I thought that was why they worded it that way instead of saying "no user-**defined** constructor". – Kyle Strand Sep 10 '16 at 14:53