purpleKarrot Gedankenexperimente

Virtuality 2026

While reviewing and refactoring the Bitcoin Kernel API, I wrote several design documents. One topic I have not yet covered is the concept of "interfaces."

Interfaces in C++

Why the quotation marks around "interface"? The way you interpret this term often reveals a lot about your programming background, sometimes more than you might realize. Let me illustrate with an example: What is the "public interface" of the class Widget in the code below?

class Widget : public Gadget
{
public:
  Widget(int value);
  int function_a(int arg);
private:
  int value_;
};

void function_b(Widget const& widget, int arg);

If your answer is "Gadget," you probably have a Java or C# background. In those languages, an interface is a contract that defines a set of methods a class must implement. If you transfer that practice to C++ (incorrectly), you might think a good "interface" is an abstract base class where all member functions (the term "method" does not exist in C++) are pure virtual.

C++ natives, however, will give a different answer. They will explain that the public interface of a class is the set of functions accessible from outside the class. For Widget, this includes its constructor and the member function function_a. function_b is also part of Widget's public interface, even though it is not a member function. The compiler provides special member functions: the copy constructor, copy assignment operator, move constructor, move assignment operator, and destructor, which are also part of the public interface unless explicitly declared otherwise. Additionally, the public interface of Widget includes all non-shadowed parts of Gadget's public interface.

C++ veterans might go further and mention the "NVI" (Non-Virtual Interface) idiom, where virtual functions are typically not exposed publicly. This approach separates the specification of the interface from the specification of customizable behavior. There is a significant difference between thinking that all functions of an interface should be virtual and the notion that an interface should not contain any virtual functions.

Real-World Practice

In his article "Virtuality", Herb Sutter shows that the C++ standard library contains only six public virtual functions, all of which were added very early in the library's development. Most virtual functions are nonpublic. He concludes that this shows "how we as a community have learned over the years." But apart from the C++ standard library and maybe Boost, have you ever come across a codebase that follows the guidelines laid out in that article? I have not.

I conclude that this shows "how we as a community have failed to counter The miseducation of C++ over the last 25 years."

With C++26 around the corner, those guidelines deserve an update. Instead of using the template method pattern, where public member functions are forwarded to private virtual functions of the same class, I suggest separating the specification of the interface from the specification of customizable behavior into two distinct classes.

In practice, a library should usually expose only one of these classes to clients: either a non-virtual interface for direct use, or an abstract base class as a customization point. It is uncommon for clients to need both at the same time.

This approach avoids naming conflicts and eliminates the need for decorations like "Interface," "Base," or "Do." For example, a logging library will provide a concrete Logger class for clients to use directly. In contrast, a framework or extensible system might provide a Logger abstract base class for clients to implement custom logging behavior. Since only one of these classes is exposed, it can be named Logger without ambiguity or extra decoration.

Since the abstract base class is not used as an interface (this may require a second look if you have always considered "interface" and "abstract base class" to be synonymous), there is no harm in making its virtual functions public. This is also why a proposal to prefer making virtual functions private in the C++ Core Guidelines was rejected.

The Bitcoin Kernel

Before finishing the updated guidelines, I want to dig into the code that actually motivated this writing: the Bitcoin Kernel API. That API currently defines two extension points: one for validation and one for notifications. While I could comment on the fact that it refers to them as "interfaces," my bigger concern is with this line in particular. Can we actually reason about the correctness of that code? We cannot, and that is a problem.

The issue with this code is that it violates the Law of Exclusivity: it modifies a variable that is shared across different parts of the system. A mutable reference to this variable is used to initialize context, which in turn is used to initialize a chainman. Since chainman launches several threads internally, we must trust that it protects the shared state with a mutex. However, because we have no access to such a mutex, we cannot be sure that reading from this variable is safe, let alone writing to it, without risking a data race.

A compiler for a memory-safe language, like Rust or Swift, would not allow this code to compile. Swift requires exclusive access to a variable to modify it, which is not given here. In Rust, if you have a mutable reference to a value, no other references to that value are allowed.

The Most Important Design Guideline is to "Make interfaces easy to use correctly and hard to use incorrectly." It is crucial to improve the extension points of the Bitcoin Kernel API to ensure safety in any language. There are two ways to prevent mutable access to shared state: either disallow sharing or disallow mutation.

The root of the problem lies right here. This is where the abstractions for validation are stored as shared and mutable objects using std::shared_ptr. From here, we can trace the function calls that pass these objects around until they are eventually stored. The resulting code is brittle and full of mutexes. While refactoring is needed, this is not the ideal place to begin.

Instead, we should design the C API as though the refactoring has already been completed. By establishing a safe and robust API up front, we can ensure that the Bitcoin Kernel API is safe to use from any language. This approach allows internal refactoring without impacting client code.

Storing and passing around an abstract base class in a std::shared_ptr will not be permitted, as this enables shared mutable access. We must choose between shared or mutable access. Let us consider what changes would be required to CValidationInterface in each case.

If we choose shared immutable access, we should use a std::shared_ptr<const>. This approach requires all virtual member functions to be marked as const. Additionally, it is advisable to delete the copy constructor and assignment operator. (I suspect the authors of the current implementation may not realize that this class has a publicly accessible copy constructor, even though the public keyword is not explicitly used.) However, this route may be too restrictive, as it limits possible implementation behaviors. Why impose constraints on how client implementations access their own private data?

A better alternative may be to opt for mutable, but exclusive access using std::polymorphic. With this approach, member functions do not need to be marked as const, but implementations must provide correct copy and move operations. Clients are free to manage their own private data as needed, and while they could violate the Law of Exclusivity by providing a flawed copy constructor, any such violation becomes the responsibility of the client.

In either case, std::shared_ptr<const> or std::polymorphic should be wrapped to separate the interface from the customizable behavior, and the Interface suffix should be omitted from the latter. The destructor of the abstract base class should be marked non-virtual and protected, since both std::shared_ptr and std::polymorphic type-erase the destructor in a control block rather than invoking it polymorphically. All other virtual functions should be public, and friend declarations should be removed. Access is controlled through the wrapper class. The wrapper class is what will be passed around and eventually stored.

Designing a Safe C API

With the refactoring plan laid out, we can now consider how to wrap the new implementation in a C API. We will consider both shared immutable access and mutable but exclusive access. In either case, a customization will be passed using two parameters: a pointer to user data and a pointer to a vtable, which is a struct containing function pointers for the customizable behavior.

For shared immutable access, the vtable will contain a function pointer for the destructor and one for each virtual function. The virtual function pointers will receive the user data as void const*, while the destructor will receive it as void*. The function that registers the client implementation will take the user data as a void*, since that is what will be passed to the destructor.

struct BtcK_ValidationVTable {
  void (*destroy)(void*);
  void (*block_checked)(void const*, ...);
  ...
};

void BtcK_ContextOptions_SetValidation(
  BtcK_ContextOptions* context_options,
  void* validation,
  struct BtcK_ValidationVTable const* validation_vtable);

For mutable but exclusive access, the vtable will also need a function pointer for cloning the user data. This function will receive the user data as void const* and return a new void*. The virtual function pointers will receive the user data as void*. The function that registers the client implementation will be the same as above.

struct BtcK_ValidationVTable {
  void (*destroy)(void*);
  void* (*clone)(void const*);
  void (*block_checked)(void*, ...);
  ...
};

Note that the library providing the extension point may defer cloning the user data until a virtual function is called (copy-on-write). This implementation detail does not affect the API.

Summary of Guidelines

  1. Separate interface from abstract base class.
  2. Make interfaces non-virtual, by wrapping std::polymorphic<ABC>.
  3. Simplify C.35: A base class destructor should be either public and virtual, or protected and non-virtual.
  4. Drop C.67 and C.130. Ensure proper copy and move semantics for all classes.
  5. Know how to wrap polymorphic types in a C API.

There is no need to wait for C++26 to become widely available. A reference implementation for std::polymorphic types is available at jbcoe/value_types and can be used in C++20.