Blog 2 min read

C++ Is Now a Feature-Rich Language: Beware of Overengineering

Share this article
C++ Is Now a Feature-Rich Language: Beware of Overengineering

Being aware of overengineering is crucial when working with a feature-rich language like C++. Overengineering occurs when developers introduce overly complex or unnecessary solutions to a problem.

C++ developers may be tempted to use as many new features introduced by recent standards as possible, which can ultimately make the code more complicated than it needs to be.

Here's an example of how C++ metaprogramming can be used to create a type-erased container with arithmetic operations evaluated at compile time. While this example shows the power and flexibility of C++ metaprogramming techniques, it might seem complicated due to the use of templates, concepts, and constexpr functions:

#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;
}

In this example:

  • We define a concept Arithmetic to constrain the template parameters to arithmetic types.
  • The AnyType class template is defined to hold any arithmetic type.
  • We provide member functions (add, subtract, multiply, divide) that perform arithmetic operations between AnyType objects of potentially different types.
  • We use constexpr to ensure that these operations are evaluated at compile time.
  • We overload the operator<< to allow streaming AnyType objects to std::ostream.

Yes, for some specific needs, code like this can be useful. But in general, do we really need such a sophisticated class to perform basic arithmetic operations? It's like building a tank to kill a fly:)

To summarize, avoid being tempted by sophisticated new features and use them only when you actually need them. And always try to follow the KISS and YAGNI principles:

Keep It Simple, Stupid (KISS) and You Ain't Gonna Need It (YAGNI) are principles that advocate for simplicity and avoiding unnecessary features until they are needed.

Share this article