2

I would like to split a class implementation into three parts, to avoid that users need to deal with the implementation details, e.g., the libaries that I use to implement the functionality:

impl.cpp

#include <api.h>
#include <impl.h>
Class::Class() {
    init();
}
Class::init() {
    myData = SomeLibrary::Type(42);
}
Class::doSomething() {
    myData.doSomething();
}

impl.h

#include <somelibrary.h>
class Class {
public:
    Class();
    init();
    doSomething();
private:
    SomeLibary::Type myData;
}

api.h

class Class {
    Class();
    doSomething();
}

The problem is, that I am not allowed to redefine headers for the class definition. This does not work when I define Class() and doSomething() only in api.h, either.


A possible option is to define api.h and do not use it in the project at all, but install it (and do not install impl.h).

The obvious drawback is, that I need to make sure, that the common methods in api.h and impl.h always have the same signature, otherwise programs using the library will get linker errors, that I cannot predict when compiling the library.

But would this approach work at all, or will I get other problems (e.g. wrong pointers to class members or similar issues), because the obj file does not match the header?

allo
  • 3,234
  • 5
  • 31
  • 53

2 Answers2

5

The short answer is "No!"

The reason: any/all 'client' projects that need to use your Class class have to have the full declaration of that class, in order that the compiler can properly determine such things as offsets for member variables.

The use of private members is fine - client programs won't be able to change them - as is your current implementation, where only the briefest outlines of member functions are provided in the header, with all actual definitions in your (private) source file.

A possible way around this is to declare a pointer to a nested class in Class, where this nested class is simply declared in the shared header: class NestedClass and then you can do what you like with that nested class pointer in your implementation. You would generally make the nested class pointer a private member; also, as its definition is not given in the shared header, any attempt by a 'client' project to access that class (other than as a pointer) will be a compiler error.

Here's a possible code breakdown (maybe not error-free, yet, as it's a quick type-up):

// impl.h
struct MyInternal; // An 'opaque' structure - the definition is For Your Eyes Only
class Class {
public:
    Class();
    init();
    doSomething();
private:
    MyInternal* hidden; // CLient never needs to access this! Compiler error if attempted.
}

// impl.cpp
#include <api.h>
#include <impl.h>

struct MyInternal {
    SomeLibrary::Type myData;
};

Class::Class() {
    init();
}
Class::init() {
    hidden = new MyInternal; // MUCH BETTER TO USE unique_ptr, or some other STL.
    hidden->myData = SomeLibrary::Type(42);
}
Class::doSomething() {
    hidden->myData.doSomething();
}

NOTE: As I hinted in a code comment, it would be better code to use std::unique_ptr<MyInternal> hidden. However, this would require you to give explicit definitions in your Class for the destructor, assignment operator and others (move operator? copy constructor?), as these will need access to the full definition of the MyInternal struct.

Adrian Mole
  • 30,672
  • 69
  • 32
  • 52
  • My problem is, that I want to avoid recursive includes to ``SomeLibrary``, just because some private members use types from the library. The library is header only, so there is no need for programs to know, that my library is using it. I currently try to use two separate headers, with an ``api.h`` header that is not included at all in my implementation. Does this cause problems because of the offset calculation you mentioned? – allo Sep 27 '19 at 13:11
  • I edited my question. An option without pimpl seems to be, that I duplicate ``impl.h`` and remove everything that's private. Then I only link with ``impl.h`` in the library and only install ``api.h``. Would this work? – allo Sep 27 '19 at 13:21
  • @allo You could (in fact, you ***should***) use the `impl.h` header for both the exported (client-mode) projects and in your own implementation files. Putting the 'same' class definition in two different headers is really asking for trouble, further down the road. – Adrian Mole Sep 27 '19 at 13:25
  • The ``impl.h`` header contains the dependencies, that I do not want to expose. Reusing the ``api.h`` header does not work because of the redefinition of the class. So I would probably either need to try using the headers only seperate or I should use the pImpl solution. – allo Sep 27 '19 at 13:27
2

The private implementation (PIMPL) idiom can help you out here. It will probably result in 2 header and 2 source files instead of 2 and 1. Have a silly example I haven't actually tried to compile:

api.h

#pragma once
#include <memory>
struct foo_impl;
struct foo {
    int do_something(int argument);
private:
    std::unique_ptr<foo_impl> impl;
}

api.c

#include "api.h"
#include "impl.h"
int foo::do_something(int a) { return impl->do_something(); }

impl.h

#pragma once
#include <iostream>
struct foo_impl {
    foo_impl();
    ~foo_impl();
    int do_something(int);
    int initialize_b();
private:  
    int b;
};

impl.c

#include <iostream>
foo_impl::foo_impl() : b(initialize_b()} {  }
foo_impl::~foo_impl() = default;
int foo_impl::do_something(int a) { return a+b++; }
int foo_impl::initialize_b() { ... }

foo_impl can have whatever methods it needs, as foo's header (the API) is all the user will see. All the compiler needs to compile foo is the knowledge that there is a pointer as a data member so it can size foo correctly.

KitsuneYMG
  • 12,247
  • 3
  • 35
  • 57
  • This looks good, but when my program tries to link against the library I get an error for ``foo_impl``, that the ``unique_ptr`` cannot delete an incomplete type, with the assert in memory: ``static_assert(0 < sizeof (_Ty)``. – allo Sep 27 '19 at 14:24
  • 1
    I needed to declare a destructor for ``foo`` and add ``~foo() = default;`` to ``impl.c``, so the foo class destroys the unique pointer in its (hidden) implementation. – allo Sep 27 '19 at 14:34
  • 1
    @allo Thanks. I'll add it. As I said, I hadn't tried to compile it. I'm glad it worked mostly though. – KitsuneYMG Oct 01 '19 at 13:14