A Non-Obvious Feature in the Syntax of Variable Declaration

C++

Take a look at this innocent looking piece of code in C++. There are no templates, no virtual functions or inheritance, but the creators of this beautiful language hid a gift right in front of us.

struct A {
  A (int i) {}
};
struct B {
  B (A a) {}
};
int main () {
  int i = 1;
  B b(A(i)); // (1)
  return 0;
}

Question: What is the type of variable b? It is not the one you would expect at first glance.

Analysis

Well, the type of variable b is definitely not B. Otherwise, why would I write this article at all? I’m not going to give you the answer right away. Instead, I will tell you how to find it without studying a thousand pages of the standard.

To begin with, let’s add some debugging:

#include 
struct A {
  A (int i) { std::cout << 'A';}
};
struct B {
  B (A a) { std::cout << 'B';}
};
int main () {
  int i = 1;
  B b(A(i)); // (1)
  return 0;
}

If we try to run this code, it turns out that nothing is printed at all. But if we replace the first line with

B b(A(1)); Suddenly, everything starts to work..

Now, let’s look closely at the compiler output, when warnings are enabled.

$ g++ -W -Wall test.cpp
x.cpp:2: warning: unused parameter ‘i’
x.cpp:6: warning: unused parameter ‘a’
x.cpp: In function ‘int main()’:
x.cpp:10: warning: unused variable ‘i’

As for the first two lines, everything is quite clear. Indeed, parameters of constructors are not used. But the last line looks really strange. How come variable i is not used, while we use it in the next line?

I guess this information could be enough to answer the question after some thinking. But if there are no right thoughts coming to our mind, why not ask the compiler? That’s when RTTI comes to the rescue.

#include 
#include 
struct A {
  A (int i) {}
};
struct B {
  B (A a) {}
};
int main () {
  int i = 1;
  B b(A(i)); // (1)
  std::cout << typeid(b).name() << std::endl;
  return 0;
}

When compiling with GCC 4.3, the result of this program is the following string:

F1B1AE It contains the information about the variable type (of course, another compiler will print a different string, the type_info::name() output format is not described in the standard and left here to the discretion of the developer). c++filt will help us to find out what these letters and numbers mean.

$ c++filt -t F1B1AE
B ()(A)

Here’s the answer: it is a function that accepts a parameter of type A and returns a value of type B.

The Reason

It remains to understand why our line is interpreted in such an unexpected way. The thing is that in the declaration of the variable type, the extra parentheses around the name are ignored. For instance, we can write

int (v); and this will mean exactly the same thing as

int v; Therefore, we can rewrite the first line without changing its meaning, by removing an extra pair of parentheses:

B b(A i); We can see now that b is the declaration of a function with one argument of type A, and that this function returns a value of type B.

At the same time, we have explained the strange warning about the unused variable i. And really, it has no relation to the formal parameter i.

Workarounds

It remains to explain the compiler what we actually want from it. That is, to get a variable of type B, initialized by a variable of type A. The easiest way is to add an extra parentheses, like this:

B b((A(i))); or like this:

B b((A)(i)); This should be enough to convince the parser that it is not a declaration of a function.

As an alternative, we can call a constructor by using an assignment operator, unless the constructor is declared as explicit:

B b = A(i); Despite the = operator, there’s no extra copy takes place here (as can be easily seen by introducing a private copy constructor in class B).

But we can also introduce an additional variable:

A a(i);
  B b(a);

However, this will require an extra copy of variable a, which is acceptable in many cases.

Choose the method that seems more clear to you :)

Inspired by the post on StackOverflow.

Comments

  1. Note this is known as the most vexing parse, which was coined by Scott Meyers in «Effective STL».

    clang does a good job of detecting vexing parse cases and generating helpful warnings, for example your code would generate the following warning see it live:

    
    prog.cc:9:6: warning: parentheses were disambiguated as a function declaration [-Wvexing-parse] 
        B b(A(i)); // (1)
           ^~~~~~
    prog.cc:9:7: note: add a pair of parentheses to declare a variable 
        B b(A(i)); // (1) 
            ^ 
            (    )
    
    

    In C++11 uniform initialization also offers an alternative: B b{A{i}}; This is probably one of the older stackoverflow questions covering it but it does not touch on all these points. I cover all these points in a newer related question here.

  2. Nice recap on the most vexing parse problem. Just a minor nitpick: when you say

    Despite the = operator, there’s no extra copy takes place here (as can be easily seen by introducing a private copy constructor in class B)

    It’s (almost always) true that no extra copy takes place, but it’s not true that you can have a private (or deleted since C++11) copy constructor as the standard explicitly requires a copy constructor to be accessible (can’t remember the exact wording, but that’s the gist of it) even though the compiler is allowed not to call it even if it has side effects. In other words, the standard allows to avoid calling a copy constructor during a copy-initialization, even if you rely on its side effects (e.g. the copy constructor does I/O), but a copy constructor must nonetheless be eligible for calling, otherwise the copy-initialization statement is ill-formed

3,751

Ropes — Fast Strings

Most of us work with strings one way or another. There’s no way to avoid them — when writing code, you’re doomed to concatinate strings every day, split them into parts and access certain characters by index. We are used to the fact that strings are fixed-length arrays of characters, which leads to certain limitations when working with them. For instance, we cannot quickly concatenate two strings. To do this, we will at first need to allocate the required amount of memory, and then copy there the data from the concatenated strings.