What Is the Most Vexing Parse in C++?
The most vexing parse is a specific type of syntactic ambiguity in C++ where a statement intended to be an object initialization is instead interpreted by the compiler as a function declaration. Coined by Scott Meyers in his 2001 book Effective STL, this issue stems from an inherited C ambiguity resolution rule that prioritizes declarations over expressions. Understanding why this occurs requires examining the grammar rules governing C++ parameter declarations and learning how modern language features, such as uniform initialization, easily prevent the bug.
The Problem Illustrated
The most famous manifestation of the most vexing parse occurs when passing a temporary object to an object's constructor:
#include <iostream>
#include <vector>
#include <iterator>
#include <fstream>
class Timer {
public:
Timer() {}
};
class Task {
public:
Task(Timer t) {}
};
int main() {
// Intended: Construct an object 't' of type Task, passing a temporary Timer.
// Actual: Declares a function named 't' returning Task.
Task t(Timer());
}Instead of instantiating Task t, the compiler treats
Task t(Timer()); as a declaration of a function named
t that returns a Task and accepts a single,
unnamed parameter of type "pointer to a function taking no arguments and
returning a Timer".
Why the Grammar Favors Function Declarations
The ambiguity arises because C++ grammar allows extraneous parentheses around variable and parameter names, a feature inherited from C:
- Redundant Parentheses in Types and Names: In C and
C++,
int (x)is functionally identical toint x. Likewise,Timer()can be evaluated as an invocation of the default constructor, but it can also be evaluated as a parameter of typeTimerwith omitted parameter names and redundant parentheses. - Ambiguity Resolution Rule: The C++ standard explicitly defines how to handle syntactic ambiguity: if an expression can be parsed as a declaration, it must be parsed as a declaration.
When the compiler encounters Task t(Timer()), it
evaluates two possible interpretations:
- Expression/Initialization: Define an object
tof typeTaskinitialized with a temporaryTimer. - Declaration: Declare a function
treturningTaskthat accepts a parameter of typeTimer()(a function returningTimer).
Because both interpretations are syntactically valid, the standard's disambiguation rule takes effect. The compiler chooses the declaration, silently converting what was supposed to be a runtime variable into a function prototype.
Common Manifestations
Another common example occurs with stream iterators:
std::ifstream file("data.txt");
// Parsed as a function declaration:
std::vector<int> data(std::istream_iterator<int>(file), std::istream_iterator<int>());Here, the first argument
std::istream_iterator<int>(file) is interpreted as a
parameter named file of type
std::istream_iterator<int>. The second argument
std::istream_iterator<int>() is interpreted as an
unnamed function pointer parameter.
A simpler, related pitfall is default initialization:
Timer t(); // Not an object 't', but a function 't' taking no arguments and returning a Timer.How to Fix It
Modern C++ provides several direct solutions to bypass the disambiguation rule:
Braced Initialization (C++11 and newer): Uniform initialization with curly braces cannot be parsed as a function declaration.
Task t{Timer{}}; std::vector<int> data{std::istream_iterator<int>(file), std::istream_iterator<int>()}; Timer t{};Named Instances: Instantiate the temporary object on a separate line before passing it.
Timer timer; Task t(timer);Extra Parentheses: Wrapping an entire argument in an extra pair of parentheses prevents the compiler from treating the contents as a parameter declaration:
Task t((Timer()));