0

I have a class template containing an inner class. I want the outer class to be the only one able to construct objects of the inner class but I need to expose the inner class to the outside so others can call a public method of its own.

Outer also defines a public interface for callback actions, in which it is supposed to provide an object of type Inner to the callee. So Outer must construct an Inner object and pass it as parameter in the callback method.

For example:

template<typename T>
class Outer {
public:
   virtual void callbackMethod(std::shared_ptr<Outer<T>::Inner> inner) = 0;

   class Inner {
   private:
       Obj obj;
       Inner(...) {} // init obj, a private member of another type
       
       std::shared_ptr<Inner> createInner() {
           std::shared_ptr<Inner> inner = std::make_shared<Inner>(...);
           ... /* do some work */ ...
           return inner;
       }

   public:
       ~Inner=default();
       void exposedMethod() {
           ...
       }
   }
}

Under certain conditions, Outer creates an Inner object and calls the callback method, which is supposed to be implemented by someone else. The callee should then be able to do something like this:

inner->exposedMethod();

Of course, the idea is not letting outsiders to be able to construct Inner objects; this is why I designed Inner as a nested class and declared its constructor and creator methods as private.

My problem is that I need my Outer class to call createInner but the compiler complains about Outer trying to access private Inner constructor. I tried to declare Outer as friend inside Inner, but the error is still there:

class Inner {
   friend class Outer<T>;
   ...
}

I have read that friendship follows other rules when applied to class templates. I am using clang with C++17.

So the question is, how could I design my classes so that class Outer can be the only one allowed to create objects of Inner type, it but at the same time be able to expose one of its methods to the outside? Please remember that Outer is a class template.

By the way, I don't care if the solution requires Inner to be a first-level class.

Thanks a lot.

  • `std::make_shared` doesn't work with an access-restricted constructor at all, because the construction happens inside the function, which is unrelated to the classes in question and therefore their friend status is not relevant. See https://stackoverflow.com/questions/8147027/how-do-i-call-stdmake-shared-on-a-class-with-only-protected-or-private-const. – user17732522 Jun 24 '22 at 20:02

1 Answers1

1

A straightforward solution that doesn't require friending make_shared is lock-and-key:

template<typename T>
class Outer {
   struct Inner_key{ explicit Inner_key() = default; };
public:
   virtual void callbackMethod(std::shared_ptr<Outer<T>::Inner> inner) = 0;

   class Inner {
   private:
       Obj obj;

   public:
       Inner(Inner_key, ...) {} // init obj, a private member of another type
       
       std::shared_ptr<Inner> createInner(Inner_key key) {
           std::shared_ptr<Inner> inner = std::make_shared<Inner>(key, ...);
           ... /* do some work */ ...
           return inner;
       }

       ~Inner=default();
       void exposedMethod() {
           ...
       }
   }
}

Now anywhere in the program (including std::make_shared) can lookup Inner::createInner and Inner::Inner, but they can't be called without creating an Inner_key instance, which being a private nested type, only Outer member functions can do.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • You would probably want to delete the default constructor of `Inner_key`. Otherwise you can still call `createInner` with `{}` as argument. – user17732522 Jun 24 '22 at 20:06
  • @user17732522: I've just marked it `explicit`, which I believe prevents creating the object without being able to name its type or having an existing object to copy from. – Ben Voigt Jun 24 '22 at 20:08
  • 1
    You can still call it using a conversion function template: `struct A { template operator T() { return T{}; } };` and then pass `A{}`. But admittedly this does not seem likely to be a problem. Access restrictions are not safe against intentional circumvention anyway. – user17732522 Jun 24 '22 at 20:14
  • 1
    (And you can obtain the type via template argument deduction on a pointer to the `createInner` function. Avoiding that would require making the parameter type a template parameter and then `static_assert`ing on it being correct or disabling it via SFINAE/constraint.) – user17732522 Jun 24 '22 at 20:21