Rester vigilant face à la sur-ingénierie est crucial lorsqu'on travaille avec un langage riche en fonctionnalités comme le C++. La sur-ingénierie se produit lorsque les développeurs introduisent des solutions excessivement complexes ou inutiles à un problème.
Les développeurs C++ peuvent être tentés d'utiliser le plus possible de nouvelles fonctionnalités introduites par les normes récentes, ce qui peut finalement rendre le code plus compliqué qu'il ne devrait l'être.
Voici un exemple de la manière dont la métaprogrammation C++ peut être utilisée pour créer un conteneur à type effacé avec des opérations arithmétiques évaluées à la compilation. Si cet exemple montre la puissance et la flexibilité des techniques de métaprogrammation C++, il peut sembler compliqué en raison de l'utilisation de templates, de concepts et de fonctions constexpr :
#include <iostream>
#include <type_traits>
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
template<Arithmetic T>
struct AnyType {
constexpr AnyType(const T& value) : value_(value) {}
template<Arithmetic U>
constexpr auto add(const AnyType<U>& other) const {
return AnyType{ value_ + other.value_ };
}
template<Arithmetic U>
constexpr auto subtract(const AnyType<U>& other) const {
return AnyType{ value_ - other.value_ };
}
template<Arithmetic U>
constexpr auto multiply(const AnyType<U>& other) const {
return AnyType{ value_ * other.value_ };
}
template<Arithmetic U>
constexpr auto divide(const AnyType<U>& other) const {
static_assert(other.value_ != 0, "Division by zero");
return AnyType{ value_ / other.value_ };
}
template<Arithmetic U>
friend std::ostream& operator<<(std::ostream& os, const AnyType<U>& any) {
return os << any.value_;
}
private:
T value_;
};
int main() {
constexpr AnyType<int> x{ 5 };
constexpr AnyType<float> y{ 2.5f };
constexpr auto addition = x.add(y);
constexpr auto subtraction = x.subtract(y);
constexpr auto multiplication = x.multiply(y);
constexpr auto division = x.divide(y);
std::cout << "Addition: " << addition << std::endl;
std::cout << "Subtraction: " << subtraction << std::endl;
std::cout << "Multiplication: " << multiplication << std::endl;
std::cout << "Division: " << division << std::endl;
return 0;
}
Dans cet exemple :
- Nous définissons un concept
Arithmeticpour contraindre les paramètres template aux types arithmétiques. - Le template de classe
AnyTypeest défini pour contenir n'importe quel type arithmétique. - Nous fournissons des fonctions membres (
add,subtract,multiply,divide) qui effectuent des opérations arithmétiques entre des objetsAnyTypede types potentiellement différents. - Nous utilisons
constexprpour garantir que ces opérations sont évaluées à la compilation. - Nous surchargeons l'
operator<<pour permettre l'envoi d'objetsAnyTypeversstd::ostream.
Oui, pour certains besoins spécifiques, un code comme celui-ci peut être utile. Mais en général, avons-nous vraiment besoin d'une classe aussi sophistiquée pour effectuer des opérations arithmétiques de base ? C'est comme construire un char pour écraser une mouche :)
Pour résumer, évitez d'être tenté par les nouvelles fonctionnalités sophistiquées et ne les utilisez que lorsque vous en avez réellement besoin. Et essayez toujours de suivre les principes KISS et YAGNI:
« Keep It Simple, Stupid » (KISS) et « You Ain't Gonna Need It » (YAGNI) sont des principes qui prônent la simplicité et consistent à éviter les fonctionnalités inutiles tant qu'on n'en a pas besoin.
