Selectively composing classes with constexpr functions, SFINAE, and parameter-pack driven private-inheritance in C++17 (Part 1)
This blog post was inspired by a use-case I encountered in an open source project where the developers wanted to offer different functionality depending on whether the product being offered was an enterprise offering versus a community offering. Under normal circumstances, preprocessor defines were used to separate functionality. However, when certain combinations of behavior were required, the preprocessor macro situation made the code much messier and harder to unit test. This post highlights a proposed solution and a trick I find particularly helpful.
The Basics
I think private inheritance is undervalued in the C++ universe, particularly now in C++17 where we have things like if-constexpr and more sophisticated compile-time capabilities. As you likely already knew, private inheritance, in the object-oriented software development universe, means that there exists a “has-a” relationship. (While I touch on the basics here a little bit, if you’re not familiar with private inheritance mechanics, the article here is also very helpful.)
Consider the following:
class Machine : private Gear {
// ...
};
In the above code, a Machine “has a” Gear. If in a particular application, say the Machine has a Gear and also a Pulley. The class might then look something like:
class Machine : private Gear, private Pulley {
// ...
};
So far so good. When someone downstream of you wants to use your Machine in a C++ program, it wouldn’t be proper to type cast your Machine to a Pulley; a Machine isn’t necessarily a Pulley. This is one of the benefits of private inheritance — you can’t cast through a base-class pointer to a derived class. An additional bonus with private inheritance is that, with ‘using’ statements, you can pull the interface of a base class into the derived class’ interface. For example:
class Machine : private Gear {
// ...
public:
using Gear::someMethod;
};
In the above example, Gear‘s someMethod() is now part of the public interface for Machine.
Towards the Maintenance Quandary:
Let’s move away from our Machine example and move onto another class, Feature. Consider the entry blurb to this blog post where I encountered a problem where I needed a Feature to function a certain way depending on whether it’s an Enterprise or Community build. For the sake of discussion, let’s say I add a Developer build also. Furthermore, accept the following:
- Enterprise functionality is provided by the EnterpriseFeatureImplementation class
- Community functionality is provided by the CommunityFeatureImplementation class
- Developer functionality is provided by the DeveloperFeatureImplementation class
Our Feature implementation class could conceivably use methods from EnterpriseFeatureImplementation, CommunityFeatureImplementation, or DeveloperFeatureImplementation. A possible implementation could be something like the following:
template< typename FEATURE_IMPL >
class FeatureImpl : private FEATURE_IMPL {
public:
using FEATURE_IMPL::featureSpecificMethod;
};
constexpr auto feature_selector() {
if constexpr( is_enterprise_build() ) {
return EnterpriseFeatureImplementation();
} else if constexpr ( is_developer_build() ) {
return DeveloperFeatureImplementation();
} else { /* if( is_community_build() ) */
return CommunityFeatureImplementation();
}
};
using Feature = FeatureImpl< decltype( feature_selector() ) >;
(For your benefit, I’ve created an execution environment for you to test and tinker with the above here: https://www.godbolt.org/z/4iRhwU.)
On the surface, the above is fairly flexible; we have a constexpr function that evaluates some function that lets us choose what interface to expose at compile-time. However, there are some things about the above that can get irritating during development cycles. Namely, the feature_selector() method needs to be updated with various criteria in order to select which interface to use. I consider this a maintenance headache (thus, a maintenance quandary.)
A more practical approach involves the use of templates and parameter packs. The goal will be to detect automatically whether a supplied class provides the functionality we need. For example:
template< typename... FEATURE_IMPLS >
class FeatureImpl : private FEATURE_IMPLS... {
... // Code goes here
};
And the above would be used like so:
using Feature = FeatureImpl< Enterprise, Developer, Community >;
But, an issue remains: How do we choose which methods via ‘using’ to bring to the derived class interface? How do we detect automatically whether the class supplies functionality for the service level (Enterprise or Community) that we want?
A Proposed Fix for Interface Method Resolution:
Earlier, I referred to the utility of being able to pull from privately inherited classes so that you could build an interface for the derived class. If we’re using a parameter pack, this clearly becomes tricky. The approach I am proposing involves the use of constexpr functions and SFINAE — let’s start with the basics (and later expand towards fold-driven broadcast methods and CRTP in Part 2!)
At the start of the blog post, I mentioned choosing between enterprise and community features in an application — a community-edition binary would not need to supply functionality that is present an enterprise-edition binary, for example. Consider code like the following — and note the comment in the code:
template< typename... FEATURE_IMPLS >
class Feature : private FEATURE_IMPLS... {
public:
// How do we choose from FEATURE_IMPLS for 'using' statements?
};
My approach (to the question posted in the comment) is to resolve whether each class within FEATURE_IMPLS fits certain criteria and “upgrade” based on whether the binary has been configured to be built for community edition or enterprise. I would use code such as the following:
template< typename... FEATURE_IMPLS >
class Feature : private FEATURE_IMPLS... {
public:
using InterfaceMethodSelector = decltype( FeatureDetails::resolve_function< FEATURE_IMPLS... >() );
using InterfaceMethodSelector::methodToUse;
};
The Resolve Function’s Meta Function:
Let’s detail what a possible resolve_function might look like. First, let’s start with a simple meta-function — in this case, we consider a meta function that determines whether a feature implementation defines a constexpr function that tells us whether a class implements an enterprise feature. We use SFINAE such that substitution failure resolves to a derivative of std::false_type.
template< typename T, typename = void >
struct HasEnterpriseFeature : std::false_type {};
template< typename T >
struct HasEnterpriseFeature< T, std::enable_if_t< T().isEnterpriseFeature() > > : std::true_type {};
The Resolve Function Itself:
The resolve function’s innards:
// If we get to the last type, just return the last type. The default
// will just be whatever lands last in the parameter pack.
template< typename FEATURE >
constexpr auto resolve_function() {
return FEATURE();
}
template< typename FEATURE_IMPL, typename FEATURE_IMPL2, typename... REST_OF_FEATURE_IMPLS >
constexpr auto resolve_function() {
if constexpr( HasEnterpriseFeature< FEATURE_IMPL >() ) {
return FEATURE_IMPL();
} else {
return resolve_function< FEATURE_IMPL2, REST_OF_FEATURE_IMPLS... >();
}
}
What the above resolve_function does is use a meta-function (HasEnterpriseFeature<T>) to determine if a member of the pack has a feature. If it doesn’t have the feature, the function moves on to the next member of the pack. Note that the above is just an example. You could create your own resolve_function and use whatever criteria you’d like to compose your derived class interface.
The final code would look something like the following:
#include <iostream>
#include <type_traits>
namespace FeatureDetails {
template< typename T, typename = void >
struct HasEnterpriseFeature : std::false_type {};
template< typename T >
struct HasEnterpriseFeature< T, std::enable_if_t< T().isEnterpriseFeature() > > : std::true_type {};
// If we get to the last type, just return the last type. The default
// will just be whatever lands last in the parameter pack.
template< typename FEATURE >
constexpr auto resolve_function() {
return FEATURE();
}
template< typename FEATURE_IMPL, typename FEATURE_IMPL2, typename... REST_OF_FEATURE_IMPLS >
constexpr auto resolve_function() {
if constexpr( HasEnterpriseFeature< FEATURE_IMPL >() ) {
return FEATURE_IMPL();
} else {
return resolve_function< FEATURE_IMPL2, REST_OF_FEATURE_IMPLS... >();
}
}
}
struct EnterpriseFeatureImplementation {
// Comment this out to make it not compile
constexpr auto isEnterpriseFeature() { return true; }
auto methodToUse() { std::cout << "Enterprise implementation!" << std::endl; }
};
struct CommunityFeatureImplementation {
constexpr auto isEnterpriseFeature() { return false; }
auto methodToUse() { std::cout << "Community implementation!" << std::endl; }
};
template< typename... FEATURE_IMPLS >
class Feature : private FEATURE_IMPLS... {
public:
using InterfaceMethodSelector = decltype( FeatureDetails::resolve_function< FEATURE_IMPLS... >() );
using InterfaceMethodSelector::methodToUse;
};
using SelectedFeature = Feature< EnterpriseFeatureImplementation, CommunityFeatureImplementation >;
int main( int argc, char *argv[] ) {
SelectedFeature f;
f.methodToUse();
return 0;
}
I have a sample of the use of the method here, on godbolt.org, an interactive online C++ environment: https://www.godbolt.org/z/4QMmMo
Be sure to tinker with the example on Godbolt. Notice that the general rule still applies with templates: If the template isn’t used, the compiler doesn’t generate code for it. Observe the advantages of composing classes this way — you don’t pay for what you don’t use.
You might still be asking some questions, however. What if you wanted to choose multiple methods from different classes? What if you wanted to execute multiple methods from subsets of the different classes. We’ll address those in Part 2.