on
Concepts in C++
Last post I shared about type traits to enable a SFINAE Class Template based Mocking to be used in the case of populating unit tests in cpp. Since C++20, the feature Concept is born. We will see how it is used in the code below, but my take on this is it’s essentially a more syntacticly nice Type Trait. But most importantly is, it has a better built in Error message (SFINAE errors are horrendeus imo)
In this example, I use a DigitalInput that will be used by a Button class (a study case of Embedded Systems). Here I assume the DigitalInput is some sort of given class from an library that we cannot modify, hence we would like to replace it with a Mock to enable non-HIL (Hardware in The Loop) tests. So I created a Concept DigitalInputLike that gets satisfied IFF any type has the init() and read() function in them. I also made an example of a BAD DigitalInput Mock that does not have the required functions, it will spit out a compilation error (nice catch concepts!)
During coding, the difference between using Type Traits compared to using Concepts is we do not need to have an addition of typename std::enable_if_t in the template typename call of the Button class. We also replace the use of typename with DigitalInputLike, and that automatically functions as a constraint.
#include <concepts>
#include <print>
// DigitalInputLike Concept
template <typename T>
concept DigitalInputLike = requires(T t) {
{ t.init() } -> std::same_as<void>;
{ t.read() } -> std::convertible_to<int>;
};
// Button Class Template (Uses Concept to Constrain it's instantiation)
template<DigitalInputLike T>
class Button {
public:
Button (T* input): m_DigitalInput(input) {}
void init() {
m_DigitalInput->init();
// rest of logic...
}
int read() {
return m_DigitalInput->read();
}
private:
T *m_DigitalInput;
};
// Digital Input (Real)
class DigitalInput {
public:
void init();
int read();
int process();
};
// Digital Input (Good Mock)
class DigitalInputMock {
public:
void init();
int read();
};
// Digital Input (Bad Mock)
class DigitalInputMock_Bad {
public:
/* void init(); */ // init() missing!
int not_read(); // read() missing!
};
auto main() -> int {
std::println("Real Digital Input, will proceed successfully");
DigitalInput digitalInput;
Button<DigitalInput> buttonWithReal(&digitalInput); // succeed
std::println("Good Mock, will proceed successfully");
DigitalInputMock digitalInputMock;
Button<DigitalInputMock> buttonWithMock(&digitalInputMock); // succeed
std::println("Bad Mock, will fail to compile if run");
DigitalInputMock_Bad digitalInputBad;
Button<DigitalInputMock_Bad> buttonWithBadMock(&digitalInputBad); // SFINAE in action! Compile error
return 0;
}
Output
(x86-64 clang 21.1.0 -std=c++23):
<source>:60:5: error: constraints not satisfied for class template 'Button' [with T = DigitalInputMock_Bad]
60 | Button<DigitalInputMock_Bad> buttonWithBadMock(&digitalInputBad); // SFINAE in action! Compile error
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:12:10: note: because 'DigitalInputMock_Bad' does not satisfy 'DigitalInputLike'
12 | template<DigitalInputLike T>
| ^
<source>:7:6: note: because 't.init()' would be invalid: no member named 'init' in 'DigitalInputMock_Bad'
7 | { t.init() } -> std::same_as<void>;
| ^
1 error generated.
Compiler returned: 1
If the buttonWithBadMock is commented out, it will churn out this
Real Digital Input, will proceed successfully
Good Mock, will proceed successfully
Bad Mock, will fail to compile if run
Discussion and feedback