Sign in to follow this  
adder_noir

Interesting Class Declaration

Recommended Posts

Hi,

I've hit the latest snag in my book and seeing as I don't have a C++ tutor to hand I thought I'd come on here and ask a few questions. Here's the code:

template <typename T>
class PtrVector {
public:
explicit PtrVector(size_t capacity)
: buf_(new T *[capacity], cap_(capacity), size_(0) {}
private:
T **buf_; // ptr to array of ptr to T
size_t cap_; // capacity
size_t size_; // size
};


The bit I really don't get at the moment is this line:

explicit PtrVector(size_t capacity)
: buf_(new T *[capacity], cap_(capacity), size_(0) {}


This *looks* to me like a constructor which takes one argument with variable name 'capacity'. I am interested in the fact that the variable type size_t is not declared until later though inside the class definition. Is this legal? Can you declare a variable type as an argument to a constructor when that variable type is only defined inside the class?

I do have more questions but I'd like to work through this one first. Thanks.

Share this post


Link to post
Share on other sites
It might be that your definition of size_t is identical to the typedef given to you by the standard, and that somehow the definition of the size_t alias creeped into your #include-hierarchy (in fact, it must [should (*)] be like that, as you are using size_t inside the class declaration already)


(*) -> "should" instead of "must", because I hear that (at least some version of) MSVC has problems implementing two phase lookup properly.

Share this post


Link to post
Share on other sites
I see, thanks for the reply. So I am not incorrect to wonder where it came from then. Your post is quite technical you obviously know alot about this stuff, but from I gleaned from it it appears that size_t might already exist or be assumed to exist prior to the class definition.

My next questions also concern the same line:

explicit PtrVector(size_t capacity)
: buf_(new T *[capacity], cap_(capacity), size_(0) {}


1)Now buf_, cap_ and size_ are all private variables within the class. So am I correct to assume that this constructor is just initialising these variables. I am rather confused by how there is no typical a = b syntax going on. I don't really get why we have private variable names and then just () brackets straight after them. For example what does cap_(capacity) do? cap_ is a private data member not a function so this syntax is strange to me. Given that buf_ is defined as:

T **buf_;

I can see that buf_(new T *[capacity],... is declaring a new array of pointers to variable type T of size capacity. That bit's ok. But not the rest.

2)What does the keyword explicit do? I get the feeling this is something I've already learned but forgotten about classes and constructors :o)

Just out of interest when he comes to implement stuff he does this, just one line for the whole class:

PtrVector<Shape> pic2(MAX);


Where 'Shape' I believe is an existing class or variable of some kind. MAX is obviously a pre-defined constant. I've never seen a class instantiated as an object using that syntax before except when using the std::vector type. Up to now I've only seen it as:

myClass newclass


So this is new to me. I'm guessing that the keyword 'explicit' has altered the way you create this PtrVector class from a simple beginner's style to a more advanced style which requires the <> brackets.

That's enough for now if I post too much I'll lose track of myself. Any input would be greatly appreciated ;o)

Share this post


Link to post
Share on other sites
1. Yes you are correct. That syntax is known as an initializer list.

2. The explicit keyword tells the compiler that the PtrVector constructor cannot be called like this:


PtrVector<int> my_vector;

my_vector = 5; // not allowed thanks to the 'explicit' keyword


Also, the PtrVector class is defined as a 'template' class. That's where the ClassName<Type> syntax is coming from. 'T' becomes the type that you insert between the < and > throughout the class's definition.

So essentially, when you write this:


PtrVector<int> my_vector(5);


You're creating an instance of PtrVector which can store 5 integers. So this:


PtrVector<Shape> my_vector(5);


Creates an instance of PtrVector which can manage 5 'Shape' objects - and yes, 'Shape' must be a predefined class or struct.

Share this post


Link to post
Share on other sites
Quote:

PtrVector<int> my_vector;

my_vector = 5; // not allowed thanks to the 'explicit' keyword

Almost, but a little misleading. It is operator=() that handles this case. The implicit constructor call to create the parameter to operator=() is what causes this to fail. A more straightforward example of how explicit works is like so:

PtrVector<int> my_vector = 5;

@OP
Implicit constructors have their uses, for example it is very convenient to initialise a std::string instance from a string literal. Outside such "basic" types, it is generally a good idea not to call constructors implicitly.

That said, the "explicit" keyword isn't often used to enforce this, probably because most complex types have constructors with more than a single argument. But its a good idea to be aware of "explicit" in case you get this error later on.
Quote:

I am rather confused by how there is no typical a = b syntax going on. I don't really get why we have private variable names and then just () brackets straight after them.

In C++, initialiser lists are preferred to assignment in the constructor body. One reason is that it is the only way to initialise const members, reference members and members with no default constructor. It is also the only way to specify parameters to any base class constructors. If you do not write an initialiser list, all members will be default initialised before the constructor body runs.

Consider a complex type, like std::string, as a member. There are a few ways to implement std::string, but let us assume that it always allocates a character array to hold its data (this is one of the simplest implementations). Look at the following code:

class Example
{
public:
Example()
{
text = "Hello, World";
}
private:
std::string text;
};


With such a std::string implementation, this will involve a call to the string default constructor, which involves an allocation. Then, a temporary string will be created for "Hello, World", which involves another allocation. The strings assignment operator will be invoked, which will allocate again, copy the string data, then deallocate the first allocation. Finally, the temporary will be destroyed, another deallocation.

Compare with:

class Example
{
public:
Example() : text("Hello, World")
{
}
private:
std::string text;
};


This constructor call involves a single call, which will only allocate once. There are no deallocations in the constructor.

For complex types, you should really use the initialiser list. Another "feature" of initialiser lists is that the compiler knows that it can only initialise members. This means that you can use the same names for the parameters as the member variables. Some people dislike this, and I understand, but it is convenient:

template <typename T>
class PtrVector {
public:
explicit PtrVector(size_t capacity)
: buffer(new T *[capacity]), capacity(capacity), size(0) {}
private:
T **buffer;
size_t capacity;
size_t size;
};


The code will work, the capacity member will be set to the input value and the buffer will be allocated with the parameters "capacity", not the (as yet unset) member value.

Share this post


Link to post
Share on other sites
Wow they're good replies! Many thanks to you both. I've just got back from the gym doing some ab exercises, I'll chew through those replies later and no doubt will return with questions for the next exciting installment of my proper education :oD

Share this post


Link to post
Share on other sites
Ok I've had a good read of all this stuff and I've condensed it down to the following points/questions.

1)An initialiser list is a good confusion free, well structured and efficient way of initialising data members and avoiding potentially unwanted default initialisation and excessive hidden allocation operations.

2)The template part:

template <typename T> (I notice the absence of ';' here)

is responsible for causing the constructor to require the variable type inside the <> brackets:

PtrVector<variable type> classInstanceName;

3)The explicit part seems I think (and this is a question not an observation) to be the part which requires the = operator to be used with a number/value to set the constructor argument to, thus producing this syntax:

PtrVector<int> my_vector = 5;

I'd appreciate, if possible, some feedback on this thanks ;o)

Share this post


Link to post
Share on other sites
Quote:
Original post by adder_noir
Ok I've had a good read of all this stuff and I've condensed it down to the following points/questions.

1)An initialiser list is a good confusion free, well structured and efficient way of initialising data members and avoiding potentially unwanted default initialisation and excessive hidden allocation operations.
Indeed.
Quote:

2)The template part:

template <typename T> (I notice the absence of ';' here)

is responsible for causing the constructor to require the variable type inside the <> brackets:

PtrVector<variable type> classInstanceName;
Not exactly. A template is like a recipe for creating a class. The recipes must have the parameters specified in their definitions. When you actually create the class, you replace the formal template parameters with actual arguments. So, when you write a line like
PtrVector<Shape> pic2(MAX);
you're declaring a variable of type class PtrVector<Shape> and the definition of that class is implicitly generated from the template you provided.

So, it's not exactly true that the template is responsible for causing the constructor to require the angle brackets so much as the syntax of a template definition requires it.
Quote:

3)The explicit part seems I think (and this is a question not an observation) to be the part which requires the = operator to be used with a number/value to set the constructor argument to, thus producing this syntax:

PtrVector<int> my_vector = 5;

Yeah, that's sorta true in a trivial way. The implicit keyword was added to the language to explicitly disable implicit type conversions. Without it, a lot of programmers might write something like this.

#include <iostream>
#include <string>

class Example1
{
public:
Example1(int i)
{ std::cerr << "example 1i: " << i << "\n"; }

Example1(const std::string& s)
{ std::cerr << "example 1s: " << s << "\n"; }
};

int main(int, char*[])
{
Example1 x("hello");
}

and then spend a lot of time in the WTF mode of debugging. Can you figure out why?

Share this post


Link to post
Share on other sites
I'm going to run that code through my compiler and see if I can figure it out. It looks to me like overloading the constructor function, which is for some reason rejected by the compiler or wrecks something at runtime.

Very good reply thanks, I'll check back when I've had a chance to try it out in code blocks.

Share this post


Link to post
Share on other sites
Interesting. This actually ran in my compiler:

#include <iostream>
#include <string>

class Example1
{
public:
Example1(int i)
{ std::cout << "example 1i: " << i << "\n"; }

Example1(const std::string& s)
{ std::cout << "example 1s: " << s << "\n"; }
};

int main()
{
Example1 x("hello");

return 1;
}


I have no idea why though. It also chose the correct constructor when I inputted an integer as the parameter for x's constructor. Seems I have much more to understand, or maybe I have a bad compiler. I'm betting this wouldn't have run is MS Visual. Seeing how some compiler's can allow bad stuff past is perhaps all the more reason to learn code properly as I am now!

Any ideas why it ran? I would have thought for all the world my compiler would have rejected an overloaded constructor function but it didn't. Frightens me to be honest, makes me realise how careful you have to be.

Share this post


Link to post
Share on other sites
No, that's expected and desired behaviour. The compiler will overload constructors based on parameter types just like any other function. There is no reason to be afraid (of this bit anyway).

It relies on the fact that std::string has a (non-explicit) constructor that takes a const char *, so can be converted to a std::string which becomes a perfectly good and non-ambigious match for Example1(const std::string&).

If there was a possible ambiguity, the compiler would give an error. For example:


class X
{
public:
X(float f);
X(double d);
};

void f()
{
X x(10); // convert int to float or double?
}


Since the compiler can convert an int to both a double and a float by implicit conversion, either constructor could be required so the call becomes ambiguous.

Equally:


class A
{
public:
A(int i){ }
};

class B
{
public:
B(int i){ }
};

class C
{
public:
C(const A &a){ }
C(const B &b){ }
};

void f()
{
C a(A(10)); // okay
C b(B(10)); // okay

C c(10); // ambiguous, error
}


The compiler has two equally valid options on the third line there so rather than guess will give an error, explaining (hopefully) what the ambiguity is.

In C++, potential ambiguity is not an error, only actual ambiguity. This applies pretty much across the board.

Share this post


Link to post
Share on other sites
Thanks for the reply. I never seen this before:

void f()
{
C a(A(10)); // okay
C b(B(10)); // okay

C c(10); // ambiguous, error
}


I noticed that in this line:

C a(A(10));

You create an instance of class 'C', give it the variable name 'a', and then you write this:

(A(10)) - you put the variable type (in this case class type 'A') inside the argument list. Up to now I've only ever seen it done like so:

C c(10); // no specification of which constructor to use = ambiguity! ;o)

That's new to me. I didn't know you could include the variable type inside argument list for the constructor to discern which constructor to use. Realistically speaking, in indsutry terms, how often do you encounter classes with overloaded constructors? Is it something that is just good to know or essential to know?

Share this post


Link to post
Share on other sites
Essential really. Most standard library classes have a variety of constructors for example. But don't get too hung up on the fact these are constructors - the overloading rules apply to any functions or class methods. The only part of the discussion above relevant specifically to constructors is the explicit keyword.

What is happening in my example that you quote is that a temporary, unnamed instance of A is being created and initialised.


A a(10); // create an instance of A called a;
A(10); // create an unnamed instance of A, lives for the duration of expression


The above second line would be useless in isolation, but is often used as part of a larger expression where an unnamed instance is required purely for the lifetime of the expression.

Share this post


Link to post
Share on other sites
Thanks for the reply. Just one last question before I move on from this to something else. I have noticed two different methods used here throughout this thread to call an explicit constructor.

One goes like this:

PtrVector<int> my_vector = 5;

and one like this:

PtrVector<Shape> pic2(MAX);

One uses the equals operator to set the argument value and the other uses () brackets to do it with no equals operator straight after the class variable name.

What's going on here? Is this just two different syntax methods of achieving the same thing?

Share this post


Link to post
Share on other sites
Quote:
Original post by adder_noir
What's going on here? Is this just two different syntax methods of achieving the same thing?


Yes.


class Foobar {
public:
Foobar( int x ) {
mValue = x;
}

private:
int mValue;
};

int main() {
// The following statements are equivalent:
Foobar a(5);
Foobar b = 5; // Same as Foobar b(5);
Foobar c = Foobar(5); // Same as Foobar c(5);
}





However, since PtrVector's constructor is declared explicit, you cannot implicitly convert a 5 to a PtrVector.

class Foobar {
public:
explicit Foobar( int x ) {
mValue = x;
}

private:
int mValue;
};

int main() {
Foobar a(5); // Ok
Foobar b = 5; // Error, cannot implicitly convert a 5 to a Foobar
Foobar c = Foobar(5); // Ok
}


Share this post


Link to post
Share on other sites
Quote:
Original post by AngleWyrm
size_t is defined in <cstring> as unsigned int.

Actually, according to the C++ standard, size_t is defined in cstddef. In practice, it's often defined as a built-in compiler typedef and often doesn't need any header. It may or may or not be an unsigned int. Ex: on MSVC 2010, building for x64, unsigned int is 4 bytes, but size_t is 8 bytes.

Share this post


Link to post
Share on other sites
Quote:
Original post by AngleWyrm
"According to the C++ standard" should probably be a link to a reputable source instead of an assertion, when arguing against the content of cpp.com's reference material.

You mean like cpp.com ;)

Share this post


Link to post
Share on other sites
Quote:
Original post by AngleWyrm
"According to the C++ standard" should probably be a link to a reputable source instead of an assertion, when arguing against the content of cpp.com's reference material.


If you want to be picky, then it's section 18.1, page 343 of ISO/IEC 14882:2003, the international standard defining the C++ language.


Quote:
Original post by AngleWyrm
And both places define it to be an unsigned integer.

You're not familiar with the idea of "counter examples" are you? The MSVC C++ when compiling for x64 compiler defines unsigned int to be 4 bytes. size_t is 8 bytes. Unless you live in a universe where 4 is equal to 8, obviously size_t can't be the same as an unsigned int on that compiler.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this