The Quirky Side of C++: Weird Stuff That Makes Us Love (and Hate) It
Subham Behera
Posted on July 5, 2024
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);
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};
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()'
}
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!
}
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)
}
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
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();
}
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)'
}
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)"
}
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!
Posted on July 5, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.