2

Abstract (virtual) interfaces allow you to explicitly declare a set of methods which a child class must implement in order to compile and be used with other parts of the code. This works great for most problems, except with one drawback, you must pass/store pointers/references to the interface class rather than the class itself. This is because the compiler has no way of knowing the size of the implementing type at compile time.

However, this may not be suitable for some applications. For example consider a Mutex class (assume it's similar to std::mutex). I want to be able to instantiate and use instances of Mutex directly throughout my code (i.e., I don't want to create them externally and pass them into constructors via pointers/references). For example:

class MyClass
{
private:
    Mutex mutex_;
};

But, I also want my code to be portable/independent of the specific Mutex implementation (e.g., Mutex could be implemented for Linux, Windows, FreeRTOS, etc.).

This is the same problem you would come across when using templates like so:

template <typename Mutex>
class MyClass
{
private:
    Mutex mutex_;
};

Except I don't want everything to be a template (that gets very messy and slow to compile very quickly).

The Mutex class has an implicit interface. In other words, as long as a version of Mutex exists at compile-time that has the same set of methods, it will work without issue.

I am wondering, is there a way to somehow explicitly (in code) declare this interface?

One option I know of is the PIMPL pattern. You could implement Mutex as so:

//Mutex.hpp
class Mutex
{
    class Impl;

public:
    void lock();
    void unlock();

private:
    Impl* impl_;
};

//std/Mutex.cpp
#include <Mutex.hpp>
#include <mutex>
class Mutex::Impl
{
public:
    void lock() { mtx_.lock(); }
    void unlock() { mtx_.unlock(); }

private:
    std::mutex mtx_;
};

Mutex::Mutex():
    impl_(new Impl)
{

}

void Mutex::lock()
{
    impl_.lock();
}

void Mutex::unlock()
{
    impl_.unlock();
}

Note: I'm not sure if my syntax is 100% correct, this is just meant to be an illustrative example.

Every implementation would then have a Mutex.cpp file like above with its appropriate implementation to which the lock/unlock calls are forwarded.

Unfortunately, since you have to dynamically allocate Impl, it's basically the same as if I had just passed in a reference in the first place and it's not suitable for applications which do no/should not use the heap (i.e., embedded systems).

The only other "solution" I have is something like the following:

//Mutex.hpp
#include <portable/Mutex.hpp>

/*
* The Mutex class must have the following interface
*
* class Mutex
* {
* public:
*     void lock();
*     void unlock();
* };
*
*/

This is just a "wrapper" file which includes the implementation and documents what the interface should be.

6
  • You may be looking for C++20 "concepts", which lets you constrain template parameters. Someone posted a great answer here to that effect, but then deleted it :( Another pattern I've seen is a "trait" template that wraps an implementation with an uniform interface – somewhat like pimpl but without the encapsulation or type erasure. But in simple cases you can just define an abstract base class from which all implementations shall inherit.
    – amon
    Commented Dec 13, 2023 at 22:11
  • 1
    The concept answer is indeed good, it just needed modification to remove the template usage. Instead, the Mutex.hpp should include portable/Mutex.hpp and afterwards do a static_assert that the defined mutex class satisfies the concept. Commented Dec 14, 2023 at 8:32
  • 1
    However, I think OP is overthinking this issue. The platform-specific implementations aren't user extension points where you have to be paranoid about correctness. Presumably, any such code would still be in the main repository, reviewed by the original author, and compiled at least every now and then. You get lots of compile errors, you need to fix the implementation. Commented Dec 14, 2023 at 8:34
  • "not suitable for applications which do no/should not use the heap" You can use placement new to have the pimpl be a subobject of the enclosing class, although that might remove the ABI compatibility
    – Caleth
    Commented Dec 14, 2023 at 9:21
  • @Caleth I had toyed with this idea in the past using memory pools (i.e., placement new). However, I'm not sure how you overcome the fact that the memory in which to perform the placement new still needs a size which you can't know exactly unless you have the declaration of the implementation. Commented Dec 14, 2023 at 15:27

2 Answers 2

2

You can use C++20 concepts to add an assertion that your platform mutex really fulfills the requirements.

#include <type_traits> 

//#include "platform/Mutex.hpp"
class Mutex {
public:
    void lock();
    void unlock();
};

namespace impl {

template <typename T>
concept MutexCheck = requires (T& t) {
    t.lock();
    t.unlock();
    requires std::is_default_constructible_v<T>;
};

static_assert(MutexCheck<Mutex>,
 "platform Mutex class does not satisfy interface");

}
3

The pimpl idiom is for two situations:

  • You want to insulate the clients from binary-incompatible changes to the implementation.
    Whether you can only select the proper implementation or at runtime, or want to allow completely independent improvement of the implementation.

  • You want to insulate the client from most of the code in the implementation, removing many weighty dependencies and thus speeding up compilation.

Neither of them matches yours, as you just want to switch platform dependent code when compiling without regard for the ABI, but preserving the API, including its definition.

Luckily, there is a different way to go about it:

Architecture dependent directories.

  • Have all common code in a common tree.
  • Collect all potentially platform-dependent state and extensions in their own private classes (intermediary/additional bases, private members) and functions, distributes among as many shadowable files as seems expedient.
  • Have a shadow tree per platform for platform-dependent code and replacement.
  • Compile all source files in the common and current platform tree, making the compiler take the include-files first from the platform tree, and secondary from the common tree.
4
  • In this case, do I still have the problem of needing soft (i.e., not actual code) documentation for the interfaces that need to be implemented? Commented Dec 13, 2023 at 21:39
  • 1
    Why? The interface is public, the implementation just uses hooks to platform-specific code in the platform tree. Commented Dec 13, 2023 at 22:28
  • 1
    @PatrickWright: you can provide a "example-platform" source tree, with sample classes (no real implementation, just a public interface), which is compilable, Those samples can serve as a "template" (in the general sense) and documentation for a real platform specific implementation.
    – Doc Brown
    Commented Dec 14, 2023 at 6:46
  • 1
    I would argue that the second situation for pimpl absolutely could apply here, since a mutex implementation will have a dependency on platform headers, which can be quite heavy indeed. Commented Dec 14, 2023 at 8:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.