Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

"using" declaration as a shorthand for defining constructors

105 views
Skip to first unread message

Christiano

unread,
May 27, 2017, 1:59:09 AM5/27/17
to
The book PPP2 at page 456, 2nd edition, Fourth Print, shows the following code:

struct Open_polyline : Shape {
using Shape::Shape;
void add(Point p) { Shape::add(p); }
};

and says:

"
" The declaration *using Shape::Shape* is a *using* declaration. It says that an
" *Open_polyline* can use the constructors defined for *Shape*. *Shape* has a default
" constructor and an initializer-list constructor, so the *using* declaration
" is simply a shorthand for defining those two constructors for *Open_polyline*.
" As for *Lines*, the initializer-list constructor is there as a shorthand for an initial
" sequence of *add()*s
"

Here is the class Shape (presented at page 494). What matters are the constructors only (look for CONSTRUCTOR 1 and CONSTRUCTOR 2 comments):
// -----------BEGIN Class Shape-----------------------------
class Shape { // deals with color and style, and holds sequence of lines
public:
void draw() const; // deal with color and draw lines
virtual void move(int dx, int dy); // move the shape +=dx and +=dy

void set_color(Color col);
Color color() const;

void set_style(Line_style sty);
Line_style style() const;

void set_fill_color(Color col);
Color fill_color() const;

Point point(int i) const; // read only access to points
int number_of_points() const;

Shape(const Shape&) = delete; // prevent copying
Shape& operator=(const Shape&) = delete;

virtual ~Shape() { }
protected:
Shape() {}; // <<----------- CONSTRUCTOR 1
Shape(initializer_list<Point> lst); // <<----------- CONSTRUCTOR 2

virtual void draw_lines() const; // draw the appropriate lines
void add(Point p); // add p to points
void set_point(int i,Point p); // points[i]=p;
private:
vector<Point> points; // not used by all shapes
Color lcolor {static_cast<int>(fl_color())};// color for lines and characters (with default)
Line_style ls {0};
Color fcolor {Color::invisible}; // fill color
};
//-------------- END Class Shape ---------------------------------

What I understood:

struct Open_polyline : Shape {
using Shape::Shape;

void add(Point p) { Shape::add(p); }
};

IS EQUIVALENT TO

struct Open_polyline : Shape {
Open_polyline() {};
Open_polyline(initializer_list<Point> lst);

void add(Point p) { Shape::add(p); }
};

So I decided to do a little test with a much smaller code (without graphic libraries):

//----------------- BEGIN TEST ----------------------
#include <iostream>
using namespace std;

class animal
{
protected:
animal()
{
cout << "Animal constructed" << endl;
}
};

struct bird: animal
{

#ifdef TEST
using animal::animal;
#else
bird()
{
cout << "Bird constructed" << endl;
}
#endif

};

int main()
{
bird x{};

return 0;
}
//--------------- END TEST -------------------------

First test:
$ CC -std=c++11 test.cpp
$ ./a.out
Animal constructed
Bird constructed
$

Ok. TEST has not been defined yet, so the compiler will interpret bird as:

struct bird: animal
{
bird()
{
cout << "Bird constructed" << endl;
}
};

And by inheriting "animal", the animal default constructor will be called too.
So the result was as expected.

However, when I set TEST I would expect the following output result:
Animal constructed
Animal constructed

because "using animal :: animal;" should create a constructor to "bird" using the "animal" constructor,
as the book has said.

That is,

struct bird: animal
{
using animal::animal;
};

would be EQUIVALENT TO

struct bird: animal
{
bird()
{
cout << "Animal constructed" << endl;
}
};

Let's do the test 2:

$ CC -std=c++11 -DTEST test.cpp
$ ./a.out
Animal constructed
$

(Note the -DTEST, it works like #define TEST 1)

Notice that the output was NOT what I expected; the result was the same as if "using animal::animal" did not exist.

Is my understanding of that part of the book wrong?

_________________________________________________________________________
Notes:
- *word* means word with bold style
- PPP2's website: http://www.stroustrup.com/Programming/

Alf P. Steinbach

unread,
May 27, 2017, 3:16:47 AM5/27/17
to
On 27-May-17 7:58 AM, Christiano wrote:
> [snip]
> That is,
>
> struct bird: animal
> {
> using animal::animal;
> };
>
> would be EQUIVALENT TO
>
> struct bird: animal
> {
> bird()
> {
> cout << "Animal constructed" << endl;
> }
> };
>
> Let's do the test 2:
>
> $ CC -std=c++11 -DTEST test.cpp
> $ ./a.out
> Animal constructed
> $
>
> (Note the -DTEST, it works like #define TEST 1)
>
> Notice that the output was NOT what I expected; the result was the same
> as if "using animal::animal" did not exist.
>
> Is my understanding of that part of the book wrong?

Yes.

struct bird: animal
{
using animal::animal;
};

is equivalent to

struct bird: animal
{
bird()
: animal{} // Invoke base class constructor
{}
};

and for a constructor with arguments, with really perfect forwarding of
arguments up to the base class' constructor.

I.e. it's not like a code duplication, it's logically like a forwarding
call. That it works logically like a forwarding call is important for
setting up a correct vtable pointer in an instance of a polymorphic
class. But except for that one can think of it as if the base class
constructors are simply /made available/ in the derived class (recall
that constructors don't have names, so this works nicely).

You can't express the forwarding call yourself as a templated
constructor, since C++ doesn't have really perfect forwarding. With the
forwarding that you can express, one can, for example, have a
constructor call that yields a nullpointer argument for direct use of
the base class constructor, but yields an integer literal and
compilation error for use of the forwarder in the derived class. So
that's a nice reason for existence of the feature.

On the other hand, as you illustrated by having a `protected`
constructor in the base class, the base class distinctions of
accessibility are lost, because the `using` declaration works
conceptually in the same way as a `using` declaration for a named base
class function, just making it available with the accessibility that's
in effect at the point of the `using` declaration.

Before we got `using` declarations (I think in the first
standardization, C++98) there was a way to redeclare a named base class
member in order to change its accessibility in a derived class. I think
that's no longer valid, but I'm not sure. It may be just deprecated.


Cheers & hth.,

- Alf

Christiano

unread,
May 28, 2017, 2:08:52 AM5/28/17
to
On 05/27/17 04:16, Alf P. Steinbach wrote:
> On the other hand, as you illustrated by having a `protected` constructor in the base class, the base class distinctions of accessibility are lost,
> because the `using` declaration works conceptually in the same way as a `using` declaration for a named base class function, just making it available
> with the accessibility that's in effect at the point of the `using` declaration.

May I disagree on this point?

See this link:
http://en.cppreference.com/w/cpp/language/using_declaration

It says:
"
Using-declaration introduces a member of a base class into the derived class definition, such as to expose a protected member of base as
public member of derived.
"

BUT the using-declaration has a different function when working with constructors. From the same link:

"
If the using-declaration refers to a constructor of a direct base of the class being defined (e.g. using Base::Base;), constructors of that
base class are inherited, according to the following rules:
[...]
All candidate inherited constructors that aren't the default constructor or the copy/move constructor and whose signatures do not match
user-defined constructors in the derived class, are implicitly declared in the derived class. <<====== ! I will use this at the end of the post !
[...]
It has the same access as the corresponding base constructor.
"

That is:
1- Using-declarate works to constructors as a possibility of inheritance. This function does not matter to other methods because the other methods are
naturally inherited when access is granted. It doesn't change the access.

2- Using-declarate works to others methods as a manner to expose a member of base class in derived class.

Let's return to initial code:
// ---------code 1----------------------
#include <iostream>
using namespace std;

class animal
{
protected:
animal()
{
cout << "Animal constructed" << endl;
}
};

struct bird: animal
{
using animal::animal;
};

int main()
{
bird x{};

return 0;
}
// ---------- end code 1 --------------

Firstly, animal has a default constructor, re-read the rule:
*** [...]All candidate inherited constructors that aren't the default constructor[...] ***

So, the code 1 is equivalent to:

// ---------code 2----------------------
#include <iostream>
using namespace std;

class animal
{
protected:
animal()
{
cout << "Animal constructed" << endl;
}
};

struct bird: animal
{
// nothing
};

int main()
{
bird x{};

return 0;
}
// ---------- end code 2 --------------

$ CC -std=c++11 test.cpp
$ ./a.out
Animal constructed
$

Let's do a little change on Code 1:

// ---------code 3----------------------
#include <iostream>
using namespace std;

class animal
{
protected:
animal(int x)
{
cout << "Animal constructed" << x << endl;
}
};

struct bird: animal
{
using animal::animal;
};

int main()
{
bird x{3};

return 0;
}
// ---------- end code 3 --------------

As we saw, the inherited constructor "has the same access as the corresponding base constructor".
So, code 3 will not work because animal constructor is *protected*.

$ CC -std=c++11 test.cpp
test.cpp:20:14: error: calling a protected constructor of class 'bird'
bird x{3};
^
test.cpp:15:23: note: implicitly declared protected here
using animal::animal;
^
1 error generated.
$

Changing from *protected* to *public*:

// ---------code 4----------------------
#include <iostream>
using namespace std;

class animal
{
public:
animal(int x)
{
cout << "Animal constructed" << x << endl;
}
};

struct bird: animal
{
using animal::animal;
};

int main()
{
bird x{3};

return 0;
}
// ---------- end code 4 --------------

$ CC -std=c++11 test.cpp
$ ./a.out
Animal constructed3
$

It works now because the animal constructor is public.

Testing if other method has the same behavior...
Lets add a function f with protected access:

// ---------code 5----------------------
#include <iostream>
using namespace std;

class animal
{
public:
animal(int x)
{
cout << "Animal constructed" << x << endl;
}
protected:
void f(int x) // function protected added
{
cout << "f hello" << x << endl;
}
};

struct bird: animal
{
using animal::animal;
using animal::f; // <--- Observe here
};

int main()
{
bird x{3};

x.f(3);

return 0;
}
// ---------- end code 5 --------------

As we saw, the code 5 will "expose a protected member of base as public member of derived."
A *Different* behavior when compared to constructors.

$ CC -std=c++11 test.cpp
$ ./a.out
Animal constructed3
f hello3
$
____________________________________________

My conclusion:

This using-declaration of the book PPP2 page 456 is wrong because it is trying "expose a protected member of base as public member of derived" through
using-declaration BUT this member is a constructor that, as the cppreference says, in this situation "has the same access as the corresponding base
constructor" (protected).

Alf P. Steinbach

unread,
May 28, 2017, 7:42:31 AM5/28/17
to
On 28-May-17 8:08 AM, Christiano wrote:
[...]
> It has the same access as the corresponding base constructor.
> "

Oh. I didn't know. Thanks!

Cheers!,

- Alf

bitrex

unread,
May 28, 2017, 8:39:16 AM5/28/17
to
So what's the "best practice" for calling constructors of
wrappers/abstract class implementations where there are say multiple
constructors taking parameters declared as protected in the base class?

In the past in C++11 I've used perfect-forwarding constructors with
variadic templates in the derived class to explicitly forward arguments
to the base class constructor. But then there's this to watch out for:

<https://mpark.github.io/programming/2014/06/07/beware-of-perfect-forwarding-constructors/>
0 new messages