c++inheritanceencapsulationfriendmaintainability

Invoke overridden virtual methods of a derived class without exposing them


I would like to write a function f which invokes two overridden virtual methods, op_1 and op_2, of a derived class in a particular order, without exposing those methods beyond f and the methods' containing classes.

I can think of a few approaches which work or almost work, but I'm also new to c++ and would like some help choosing the "best" way in terms of conventionality, maintainability, etc:

  1. Make f a public non-virtual method of the base class (Base) which invokes the virtual ops in order, and make the ops private.
/***** .h *****/
class Base {
  virtual void op_1() const = 0;
  virtual void op_2() const = 0;
  
public:
  void f();
};

/***** .cpp *****/
void Base::f() {
  op_1();
  op_2();
}
  1. Make the ops public and expose a non-member non-friend helper to invoke them in order.
/***** .h *****/
struct Base {
  virtual void op_1() const = 0;
  virtual void op_2() const = 0;
};

void f(const Base& b,) {
  b.op_1();
  b.op_2();
}
  1. Leave the ops private and expose a friend function to invoke them in order.
/***** .h *****/
class Base {
  virtual void op_1() const = 0;
  virtual void op_2() const = 0;
  friend void f(const Base& b);
};

void f() {
  b.op_1();
  b.op_2();
}

#1 seems bad because derived classes also have access to f, but it's not intended or useful for them and could be misused.

#2 resolves that, but is less encapsulated because the ops are public.

#3 seems to resolve both concerns, but I'm new to C++ and standoffish about friending things - it seems to muddle interfaces and the guidance I've read (e.g. Effective C++ #23) discourages its use without clear need. Is this an appropriate case for it?

#4 ??? Probably there's a better way I can't see. Enlighten me!

App-specific context, in case you want the "Why"

I need to register some data types with a 3rd party data store on app initialization - there are 2 synchronous operations I need to perform in order:

  1. store.data_type<MyAppDataType>() adds a table for an app-specific data type to the store.
  2. store.data_type<MyAppDataType>().member<MemberDataType>("member_name") yields the app-specific data type registered in step 1 and sets up one of its fields for use with the library's reflection APIs - useful for debugging in non-production builds.

Operation #2 is optional, but if performed it should occur after operation #1.

I want the various modules[1] of my application to register their own data with the store to avoid having a big messy file where all the data for the app is registered together. To do this, I am including a class in each module which extends a DataRegistrar base class - the base forces derived classes to provide module-specific implementations for the two operations above:

struct DataRegistrar {
  virtual void register_data_types(lib::store& store) const = 0;
  virtual void setup_reflection(lib::store& store) const = 0;
};

Elsewhere in the app, I can have some code which runs on app startup which invokes register_data_types and setup_reflection, in that order, for each module. Here are the approaches which map to those described above, in the simplified question:

  1. (Maps to #1 above)
/***** .h *****/
class DataRegistrar {
  virtual void register_data_types(lib::store& store) const = 0;
  virtual void setup_reflection(lib::store& store) const = 0;
  
public:
  void register_data(lib::store& store);
};

/***** .cpp *****/
void DataRegistrar::register_data(lib::store& store) {
    register_data_types(store);
#if !PROD_BUILD
    setup_reflection(store);
#endif
}
  1. (Maps to #2 above)
/***** .h *****/
struct DataRegistrar {
  virtual void register_data_types(lib::store& store) const = 0;
  virtual void setup_reflection(lib::store& store) const = 0;
};

void register_data(const DataRegistrar& module_registrar, lib::store& store) {
    module_registrar.register_data_types(store);
#if !PROD_BUILD
    module_registrar.setup_reflection(store);
#endif
}
  1. (Maps to #3 above)
/***** .h *****/
class DataRegistrar {
  virtual void register_data_types(lib::store& store) const = 0;
  virtual void setup_reflection(lib::store& store) const = 0;
  friend void register_data(const DataRegistrar& module_registrar, lib::store& store);
};

void register_data(const DataRegistrar& module_registrar, lib::store& store) {
    module_registrar.register_data_types(store);
#if !PROD_BUILD
    module_registrar.setup_reflection(store);
#endif
}

[1] "modules" as in areas/directories/domains of the app, not c++20 modules


Solution

  • Let's start from #3:

    friend breaks encapsulation and that's why Scott Meyers doesn't suggest it. I also don't suggest it unless you really need it. Eg: CRTP's private contstruct + friend trick to prevent typo.

    I would suggest #1 since all the works are done by the class extending the inerface DataRegistrar. Also you don't want others to fiddle with op_1() and op_2().

    If using free function, it requires the instance having op_1() and op_2() to be exposed and increases the chance to be misused.