Home > Mobile >  C header dependency propagation - how to limit it?
C header dependency propagation - how to limit it?

Time:11-30

I have a repeating dilemma while constructing a class in C . I'd like to construct the class without propagating its internal dependencies outside.

I know I have options like:

  1. Use pimpl idiom
  2. Use forward declaration and only reference or smart pointers in header
// header

class Forwarded;    // I don't want to include Forwarded.h to not propagete it 

class MyNewClass {
private:
   std::unique_ptr<Forwarded> mForwareded;
   void method1UsesForwarded();
   void method2UsesForwarded();
public:
   void doSomeAction();
};
// cpp
#include "Forwarded.h"
void MyNewClass::doSomeAction() {
   method1UsesForwarded();
   method2UsesForwarded();
}

void MyNewClass::method1UsesForwarded() { /* implementation */ }
void MyNewClass::method2UsesForwarded() { /* implementation */ }
  1. Create another class or helper file which uses those files which I don't want to propagate further
// header

class MyNewClass {
public:
   void doSomeAction();
};

// cpp
#include "helper.h"
void MyNewClass::doSomeAction() {
    Forwarded f;
    method1UsesForwarded(f);
    method2UsesForwarded(f);
}
// helper.h
#include "Forwarded.h"
void method1UsesForwarded(Forwarded & f);
void method2UsesForwarded(Forwarded & f);
// helper.cpp
#include "helper.h"
void method1UsesForwarded(Forwarded & f) {
    //implemntation
}
void method2UsesForwarded(Forwarded & f) {
    //implemntation
}

Is there any other option? I don't like any of the above solutions because they provide some additional complication. The best option for me would be creation of Forwarded as normal private member and somehow not propagate it further :-)

CodePudding user response:

The underlying issue is that the consumers of MyNewClass need to know how big it is (e.g. if it needs to be allocated on the stack), so all members need to be known to be able to correctly calculate the size.

I'll not address the patterns you already described. There are a few more that could be helpful, depending on your use-case.


1. Interfaces

Create a class with only the exposed methods and a factory function to create an instance.

Upsides:

  • Completely hides all private members

Downsides:

  • requires heap allocation
  • lots of virtual function calls

Header:

class MyClass {
public:
  static MyClass* create();

  virtual ~MyClass() = default;
  virtual void doSomeAction() = 0;
};

Source:

class MyClassImpl : public MyClass {
public:
  void doSomeAction() override { /* ... */ }

private:
  void method1UsesForwarded() { /* ... */ }
  void method2UsesForwarded() { /* ... */ }

  Forwarded f;
};

MyClass* MyClass::create() {
  return new MyClassImpl();
}

2. Subclass Implementation

This is somewhat similar to the Interfaces one, with the upside that it gets rid of the virtual functions.

Upsides:

  • Completely hides all private members

Downsides:

  • requires heap allocation

Header:

class MyClass {
public:
  static MyClass* create();

  // only the destructor needs to be virtual now
  virtual ~MyClass() = default;

  void doSomeAction();

private:
  MyClass() = default;
};

Source:

class MyClassImpl : public MyClass {
public:
  void method1UsesForwarded() { /* ... */ }
  void method2UsesForwarded() { /* ... */ }

  Forwarded f;
};

void MyClass::doSomeAction() {
  // e.g.:
  static_cast<MyClassImpl*>(this)->method1UsesForwarded();
  // ...
}

MyClass* MyClass::create() {
  return new MyClassImpl();
}

3. Pre-allocating space for private members

If you don't mind a bit of manual work you can calculate how much space the private members will need and just provide a large enough buffer for that in the base class.

Upsides:

  • Completely hides all private members
  • Does not require a heap allocation

Downsides:

  • You need to manually check how large a buffer you need
  • Required size might change when using different compilers / different compiler versions

Header:

class MyClass {
public:
  MyClass();
  ~MyClass();
  void doSomeAction();
private:
  void method1UsesForwarded();
  void method2UsesForwarded();

  struct MyClassMembers* getMembers();
  
  /*
    here you need to enter the size and alignment requirements
    of your private data buffer.
  */
  std::aligned_storage_t<8, 4> m_members;
};

Source:

struct MyClassMembers {
    int i = 0;
    int j = 0;
};

MyClass::MyClass() {
  static_assert(sizeof(m_members) >= sizeof(MyClassMembers), "size too small!");
  static_assert(alignof(m_members) >= alignof(MyClassMembers), "alignment too small!");

  new (&m_members) MyClassMembers();
}

MyClass::~MyClass() {
    getMembers()->~MyClassMembers();
}

MyClassMembers* MyClass::getMembers() {
  return std::launder(reinterpret_cast<MyClassMembers*>(&m_members));
}

void MyClass::doSomeAction() {
  method1UsesForwarded();
  /* ... */
}

void MyClass::method1UsesForwarded() {
  /* ... */
  std::cout << getMembers()->i << std::endl;
}


void MyClass::method2UsesForwarded() {
  /* ... */
}

One gotcha of this approach is that you need to provide the correct size & alignment for your data struct (in this case MyClassMembers).
You can either eyeball it (the static_asserts ensure that it won't compile if the requirements are not met) by trying to compile with different values until it works, or write a short function that prints out the correct values for you:

struct MyClassMembers {
  int i;
  int j;
};

int main(int argc, char* argv[]) {
  std::cout << sizeof(MyClassMembers) << std::endl;
  std::cout << alignof(MyClassMembers) << std::endl;
}

CodePudding user response:

I would suggest using private inheritance. By doing that it makes all member functions of the base class private in the in inherited class (except in constructors, there is no hiding until constructor allocation and initializers complete).

If you do that, then no calls to base class members can be made from outside code. Public and protected members of the base class are private in the derived class. Private members of the base class are inaccessible to the derived class entirely.

A compromise is protected inheritance, actually very seldom used as it really has only niche use. Protected inheritance renders the public and protected members of the base class protected in the derived class, hiding them from outside code, but propagating access downward into further derived classes.

You really have to plan out the class hierarchy as to what derived level is to have access to what functions with a mix of public, protected, and private members.

The common scenario is to make base class public functions private in the derived class if you want to make sure they will not be called in code with the derived class. This is the opposite of what most people do; they usually want to include base class functions as public in the derived class.

  • Related