DEV Community

Subham Behera
Subham Behera

Posted on • Updated on

The Quirky Side of C++: Weird Stuff That Makes Us Love (and Hate) It

Welcome, fellow coders, to a whimsical journey through the quirks and oddities of C++. While C++ is celebrated for its power and flexibility, it also comes with a host of peculiarities that can surprise even seasoned developers. Let's dive into some of the weird and wonderful aspects of C++ that make it the charming (and sometimes infuriating) language it is.


1. The Infamous "Most Vexing Parse"

One of the most notorious oddities in C++ is the "most vexing parse," a term coined by Scott Meyers. This quirk can turn what looks like an innocuous declaration into something entirely unexpected.

std::vector<int> v(std::vector<int>::size_type(10), 20);
Enter fullscreen mode Exit fullscreen mode

You might think this creates a vector with 10 elements, each initialized to 20. But no, it actually declares a function named v that returns a std::vector<int> and takes a parameter of type std::vector<int>::size_type and another parameter of type int.

To avoid this, use uniform initialization (a.k.a. brace initialization):

std::vector<int> v{10, 20};
Enter fullscreen mode Exit fullscreen mode

2. Default Arguments in Function Templates

Default arguments in function templates can lead to some baffling behaviour. Consider this example:

template<typename T>
void foo(T t = 10) {
    std::cout << t << std::endl;
}

int main() {
    foo(); // Error: no matching function for call to 'foo()'
}
Enter fullscreen mode Exit fullscreen mode

Even though 10 is a valid default argument for an int, the compiler doesn't know T is int until it's explicitly told. A workaround is to use an overloaded function:

void foo(int t = 10) {
    std::cout << t << std::endl;
}

template<typename T>
void foo(T t) {
    std::cout << t << std::endl;
}

int main() {
    foo(); // Works!
}
Enter fullscreen mode Exit fullscreen mode

3. The Magic of SFINAE

SFINAE (Substitution Failure Is Not An Error) is a cornerstone of C++ template metaprogramming, allowing for complex template logic. However, it can be quite perplexing at first glance.

template<typename T>
auto test(int) -> decltype(std::declval<T>().foo(), std::true_type{});

template<typename T>
std::false_type test(...);

struct HasFoo {
    void foo() {}
};

int main() {
    std::cout << decltype(test<HasFoo>(0))::value << std::endl; // Prints 1 (true)
    std::cout << decltype(test<int>(0))::value << std::endl;    // Prints 0 (false)
}
Enter fullscreen mode Exit fullscreen mode

The SFINAE magic here checks if a type T has a member function foo and sets the return type accordingly. It's a powerful feature, but can lead to head-scratching moments.


4. The Curious Case of std::vector<bool>

std::vector<bool> is a special case in the C++ Standard Library. Unlike other std::vector specializations, it doesn't store bool values directly. Instead, it uses a bit-field-like structure to save space, leading to unexpected behaviour.

std::vector<bool> vb = {true, false, true};
vb[0] = false;

std::cout << std::boolalpha << vb[0] << std::endl; // Prints false
Enter fullscreen mode Exit fullscreen mode

The non-standard representation can cause performance issues and surprising side effects. If you need a true std::vector of boolean-like values, consider using std::vector<char> or std::vector<int> instead.


5. The Hidden Cost of Copy Elision

Copy elision is an optimization technique where the compiler omits unnecessary copy and move operations. However, this can lead to some unexpected scenarios.

struct Widget {
    Widget() { std::cout << "Widget()" << std::endl; }
    Widget(const Widget&) { std::cout << "Widget(const Widget&)" << std::endl; }
    Widget(Widget&&) { std::cout << "Widget(Widget&&)" << std::endl; }
};

Widget createWidget() {
    return Widget();
}

int main() {
    Widget w = createWidget();
}
Enter fullscreen mode Exit fullscreen mode

With copy elision, the compiler might optimize away the copy and move constructors, making it seem like they are never called, even though they exist.


6. The Enigma of Name Hiding

Name hiding can cause some truly baffling behaviour. If a derived class declares a member with the same name as one in the base class, it hides all base class members with that name, even if the signatures are different.

struct Base {
    void f(int) { std::cout << "Base::f(int)" << std::endl; }
};

struct Derived : Base {
    void f(double) { std::cout << "Derived::f(double)" << std::endl; }
};

int main() {
    Derived d;
    d.f(10); // Error: no matching function for call to 'Derived::f(int)'
}
Enter fullscreen mode Exit fullscreen mode

To avoid this, bring the base class function into scope using using:

struct Derived : Base {
    using Base::f;
    void f(double) { std::cout << "Derived::f(double)" << std::endl; }
};

int main() {
    Derived d;
    d.f(10); // Prints "Base::f(int)"
    d.f(10.0); // Prints "Derived::f(double)"
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

C++ is a language full of quirks and intricacies that can be both fascinating and frustrating. These weird aspects are part of what makes C++ so powerful and versatile, yet they also highlight the importance of understanding the language deeply. Embrace the quirks, and you'll find C++ to be an endlessly rewarding language.


Feel free to share your own C++ oddities and experiences in the comments below. Happy coding!


Top comments (0)