Parentheses that change everything

It is well known — and intuitively understood by most — that adding a set of parentheses usually doesn’t change anything; for example, int answer = 42; is equal to int answer = (42); or int answer = ((42));. There are some important exceptions to that rule, however, and I’ll talk about these in this post.

Macros

Although macros are rarely used in good C++ code, it is important to be able to understand what’s happening and why. Using a popular example of MIN macro, the naïve implementation would look like this:

#define MIN(x,y) x < y ? x : y

To a beginner, this would look like a correct implementation, and indeed, it would work in some cases; for example, answer below would indeed be equal to 42:

int answer = MIN(42,50);

Unfortunately, macros are expanded as text, and in the following example, possibly surprisingly, answer would hold the value of 41 instead:

int answer = 2 + MIN(40,41);


This behaviour is caused by incorrect assumption that function macros work like functions, solely because they look the same. In reality, the above macro expands to

int answer = 2 + 40 < 41 ? 40 : 41;

and the answer as to why 41 is chosen is obvious. This is why a less naïve (and the best macro implementation that doesn’t rely on non-standard compiler extensions) MIN usually looks like this:

#define MIN(x,y) (((x) < (y)) ? (x) : (y))

Parentheses enforce evaluation of the macro as a single expression. There still is a double evaluation problem, (think what would happen when MIN(i++,b++) is called), but this isn’t the topic of this post, and, as written above, to the best of my knowledge, it’s not possible to solve this problem and have the code be legit C and C++ and non-reliant on compiler-specific extensions. It would be possible with C++11’s decltype or auto, but if we were writing for C++, we’d already use an inline function, or, even better, std::min.

Assignment in condition warning omission

Assignment in conditions are wrong. Usually. That’s why the compiler will emit a warning when you do so.

while(answer = 42)

or

if(answer = 42)

will generate a warning on gcc or clang with -Wall or -Wparentheses. I didn’t test with msvc, but any sane compiler should warn when you do that. It stands to reason, however, that sometimes such code would be exactly what was intended, and not a typo. Adding an additional set of parentheses on the expression will tell the compiler that you indeed know you’re doing an assignment and no warning will be emitted:

while((answer = 42))

It is worth nothing, that the only thing affected by parentheses are compiler diagnostics, and the generated code is exactly the same.

decltype

To be honest, this is the real reason behind writing this post. The above examples are known for years and aren’t really surprising to anyone who wrote a few lines in C or C++. The new C++ standard introduces new ways to declare types, one of them being decltype. Here’s a quote from n3337 § 7.1.6.2.4:

The type denoted by decltype(e) is defined as follows:
— if e is an unparenthesized id-expression or an unparenthesized class member access (5.2.5), decltype(e) is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed;
— otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e;
— otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;
— otherwise, decltype(e) is the type of e.
The operand of the decltype specifier is an unevaluated operand (Clause 5).

Basing on the above definition,

int a = 10;
decltype(a) b;

b is an int, but if we add an additional set of parentheses

int a = 10;
decltype((a)) c;

c becomes int&. This happens because, in b‘s case, a is “an unparenthesized id-expression” and decltype follows the first rule, while (a) is “an lvalue” forcing decltype to follow the third rule and become a reference to an int. In practice

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
 
int main()
{
	using namespace std;
	int a = 10;
	decltype(a) b = a;
	decltype((a)) c = a;
	b = 11;
	c = 12;
	cout << a << ", " << b << ", " << c << endl;
}

will print 12, 11, 12.

Leave a Reply

Your email address will not be published.