Comma Delimited Variable Declarations

Jan 4, 2017 at 3:35am
Hi all,

I was working on a project and came across something that doesn't make sense to me.

The below code doesn't compile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef HEAD_H
#define HEAD_H

template<typename T>
class splay_tree {
private:
	struct node {
		node* left,
		      right,
		      parent;
		T data;
	};
	node* root;

	unsigned int nodeCount;
public:

}; // END splay_tree	

#endif 


Trying to compile with
g++ -std=c++14 -pedantic -Wall -Wextra <filename>

Spits out the following:
error: 'splay_tree<T>::node::right' has incomplete type
and the same for parent.

However, if i do the same type of declaration with an int, for example, it compiles fine. Also, I can just swap the compound declaration to individual declarations and it compiles fine.

Why is it erring out with the Node declarations separated by commas but not the ints?
Last edited on Jan 4, 2017 at 3:35am
Jan 4, 2017 at 3:51am
1
2
3
4
5
6
7
8
struct node {

    node* left, // type is 'pointer to node'
          right, // type is 'node'
          parent; // type is 'node'

    T data;
};


This is fine:
1
2
3
4
5
6
7
8
9
struct node {

    using ptr_node = node* ;

    ptr_node left,
             right,
             parent;
    T data;
};


Ideally, stick to one name per declaration.
The critical confusion comes (only) when people try to declare several pointers with a single declaration:
int* p, p1; // probable error: p1 is not an int*

Placing the * closer to the name does not make this kind of error significantly less likely.
int *p, p1; // probable error?

Declaring one name per declaration minimizes the problem - in particular when we initialize the variables. People are far less likely to write:
1
2
	int* p = &i;
	int p1 = p;	// error: int initialized by int* 

And if they do, the compiler will complain.

http://www.stroustrup.com/bs_faq2.html#whitespace


1
2
3
4
5
6
7
8
struct node {

    node* left = nullptr ;
    node* right = nullptr ;
    node* parent = nullptr ;

    T data;
};
Last edited on Jan 4, 2017 at 3:53am
Jan 4, 2017 at 4:05am
Thank you, that definitely makes sense! I didn't know that the * symbol was, in some basic sense, attached to the variable name rather than the type. Cool side note, I tried doing
1
2
3
node* left,
    * right,
    * parent;

and that seems to work fine as well, although, as noted on the link you provided, this way seems to be more error-prone.

At this point I'm just curious as to why this won't compile
1
2
3
node* left,   // typeof(left) == node*
      right,  // typeof(right) == node
      parent; // typeof(parent) == node 

but this will compile
1
2
3
int* x, // typeof(x) == int*
     y, // typeof(y) == int
     z; // typeof(z) == int 
Last edited on Jan 4, 2017 at 4:20am
Jan 4, 2017 at 4:22am
> I'm just curious as to why this won't compile

Before the definition of node is completed, node is an incomplete type.

1
2
3
4
5
6
7
8
9
struct node {

    // ...

    // node is an incomplete type at this point

    // ...
    
}; // node is now a complete type 


And:
A class that has been declared but not defined, an enumeration type in certain contexts, or an array of unknown size or of incomplete element type, is an incompletely-defined object type. Incompletely-defined object types and cv void are incomplete types. Objects shall not be defined to have an incomplete type.
...
Non-static data members shall not have incomplete types. In particular, a class C shall not contain a non-static member of class C, but it can contain a pointer or reference to an object of class C. - IS
Jan 4, 2017 at 4:45am
OK, that kind of makes sense to me. At the risk of being annoying I'll ask one more question.

I imagine that the reason you can declare a pointer to, but not a instance of, an incomplete type is because the compiler only needs to set aside enough space for the address of the incomplete type, not the space for the incomplete type (which is still unknown). So, testing that I tried this
1
2
3
4
5
6
7
8
9
struct node {

	// ...

	node* x = new node();

	// ...
	
};

and to my surprise it compiled. Is it waiting until the definition of node is complete to allocate the space x is requesting?

Thank you for the information!
Jan 4, 2017 at 4:58am
I think it's compiler specific: with GCC 6.1
1
2
3
4
5

D:\>g++ -o test test.cpp
test.cpp:7:21: error: constructor required before non-static data member for 'node::x' has been parsed
  node* x = new node();
                   ^

However the following compiles, debugs and runs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

struct node {

	// ...
    node() {}
	node* x = new node();
	// ...

};

int main()
{

}
Last edited on Jan 4, 2017 at 5:01am
Jan 4, 2017 at 5:05am
Apologies, my complete struct is
1
2
3
4
5
6
7
8
9
10
11
12
13
struct node {
	node() = delete;
	node(T init) : left(nullptr), right(nullptr), parent(nullptr), data(init) { }
	~node() {
		if(left)   delete left;
		if(right)  delete right;
		if(parent) delete parent;
	}
	node* left,
	    * right,
	    * parent;
	T data;
};
.
Removed the extra sterfs to show what I was talking about. That being said, I removed all the CTors/DTors and nothing changed using the same g++ compilation previously posted, so I think you are right @gunnerfunner
Last edited on Jan 4, 2017 at 5:07am
Jan 4, 2017 at 5:09am
> Is it waiting until the definition of node is complete to allocate the space x is requesting?

In node* x = new node();, the brace-or-equal-initialiser is evaluated (if required) only when an actual object of type node is created; at that point node would be a complete type.

A class is considered a completely-defined object type (or complete type) at the closing } of the class-specifier.

Within the class member-specification, the class is regarded as complete within function bodies, default arguments, noexcept-specifiers, and default member initializers (including such things in nested classes).

Otherwise it is regarded as incomplete within its own class member-specification.

- IS
Jan 4, 2017 at 5:35am
nothing changed using the same g++ compilation previously posted

But try it in an actual program and you'll notice the error messages, for e.g.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
template <typename T>
struct node {

	node() = delete;
	node(T init) : left(nullptr), right(nullptr), parent(nullptr), data(init) { }
	public:
	~node() {
		if(left)   delete left;
		if(right)  delete right;
		if(parent) delete parent;
	}
	node* left ;
	    * right,
	    * parent;
	T data;
};
int main()
{
  node<int> a;

}

Error message:
 
error: use of deleted function 'node<T>::node() [with T = int]'|
Jan 4, 2017 at 12:49pm
That's a good one:
1
2
3
struct node {
	node* x = new node();
};


The standard rule this breaks is [class.mem]/8 http://eel.is/c++draft/class.mem#8

A brace-or-equal-initializer for a non-static data member specifies a default member initializer for the member, and shall not directly or indirectly cause the implicit definition of a defaulted default constructor for the enclosing class


Here "new node" causes the implicit definition of node's constructor (because it's an ODR-use) and it's not allowed.

This was actually allowed at first in C++11, the rule was introduced to address defect report CWG1397 http://open-std.org/JTC1/SC22/WG21/docs/cwg_defects.html#1397 and several releated issues all revolving around constructors needing to know if default member initializers are noexcept, constexpr etc, to determine if the constructors themselves are.
Topic archived. No new replies allowed.