Virtuality 2026
Published 17 Nov 2025While 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
- Separate interface from abstract base class.
- Make interfaces non-virtual, by wrapping
std::polymorphic<ABC>. - Simplify C.35:
A base class destructor should be
either public and virtual, orprotected and non-virtual. - Drop C.67 and C.130. Ensure proper copy and move semantics for all classes.
- 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.