royyan@rd:~/notes$
$ cat concepts.md
---
date: 2025-12-17
tags: [cpp]
---

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
$