Const member initialization list question

W

Wolfram Humann

Hi,

please don't be too harsh if I made stupid errors creating this simple
example from my more complex case.

Suppose I have a class like this:

class BOOK
{
const string title;
const string author;
const int pages;
const double price;

static map<string, BOOK> MyBooks;

BOOK(const string& title_, const string& author_,
const int pages_, const double price_);

BOOK(const string& title_);

const BOOK& findByTitle(const string& title);
}

I fill the map with books created by the 4-argument constructor. I use
findByTitle to look up books in the map. My problem is with the
constructor that takes a title. About the only way to define this
seems to be:

BOOK::BOOK(const string& title_)
: title(title_),
author(findByTitle(title_).author)
pages(findByTitle(title_).pages)
price(findByTitle(title_).price)
{}

The problem is that findByTitle is called 3 times. That's not very
efficient. If my members were non-const I could do

BOOK::BOOK(const string& title_)
: title(title_)
{
BOOK book = findByTitle(title_);
author = book.author;
pages = book.pages;
price = book.price;
}

But since my books don't have a habit of suddenly changing their
author, I think those members *should* be const. Any elegant
solutions?

Thanks,
Wolfram
 
V

Victor Bazarov

Wolfram Humann said:
please don't be too harsh if I made stupid errors creating this simple
example from my more complex case.

Suppose I have a class like this:

class BOOK
{
const string title;
const string author;
const int pages;
const double price;

Should price really be 'const'? Never mind...
static map<string, BOOK> MyBooks;
public:


BOOK(const string& title_, const string& author_,
const int pages_, const double price_);

BOOK(const string& title_);

const BOOK& findByTitle(const string& title);

This should probably be 'static'.
;

I hope those three suggestions are not an indicator of my being harsh.
I fill the map with books created by the 4-argument constructor. I use
findByTitle to look up books in the map. My problem is with the
constructor that takes a title. About the only way to define this
seems to be:

BOOK::BOOK(const string& title_)
: title(title_),
author(findByTitle(title_).author)
pages(findByTitle(title_).pages)
price(findByTitle(title_).price)
{}

The problem is that findByTitle is called 3 times. That's not very
efficient. If my members were non-const I could do

BOOK::BOOK(const string& title_)
: title(title_)
{
BOOK book = findByTitle(title_);

BOOK const & book = findByTitle(title_); // more efficient
author = book.author;
pages = book.pages;
price = book.price;
}

But since my books don't have a habit of suddenly changing their
author, I think those members *should* be const. Any elegant
solutions?

You have to answer the question: why do I need to construct another
object from a single 'title' when I already have one sitting in the
map<string, BOOK>? As soon as you can answer that, we'll be able to
give you an elegant solution. Deal?

Victor
 
W

Wolfram Humann

You have to answer the question: why do I need to construct another
object from a single 'title' when I already have one sitting in the
map<string, BOOK>? As soon as you can answer that, we'll be able to
give you an elegant solution. Deal?

Victor

Suppose the user of the class wants to say:
(assuming the book is already in the map, error otherwise)

BOOK stroustrup("The C++ Programming Language");
cout << "Stroustrup has " << stroustrup.pages << " pages" << endl;

or I have

LIBRARY::addBook(BOOK book);

myLibrary.addBook("The C++ Programming Language");

I already found that here I need a BOOK::BOOK(const char* title_) (with
char* instead of string) constructor. I think the reason is that otherwise
I have an implicit conversion chain with two user-defined conversions...?

Wolfram
 
H

Howard

Wolfram Humann said:
Suppose the user of the class wants to say:
(assuming the book is already in the map, error otherwise)

BOOK stroustrup("The C++ Programming Language");
cout << "Stroustrup has " << stroustrup.pages << " pages" << endl;

or I have

LIBRARY::addBook(BOOK book);

myLibrary.addBook("The C++ Programming Language");

I already found that here I need a BOOK::BOOK(const char* title_) (with
char* instead of string) constructor. I think the reason is that otherwise
I have an implicit conversion chain with two user-defined conversions...?

Wolfram

It seems to me that addBook (in the second case at least) should do the
lookup for the book given the title, and error out if it is not found. Then
it can create a new instance (or copy-create it, or use a reference to the
the existing one) based on the one it finds. I'm not sure what you want to
happen in the first case, since you haven't provided a constructor with no
parameters, and you're in effect adding an "unknown" book to the library.
(Also, shouldn't the map be a member of your library class, not a member of
the book class itself? It seems strange to have a book that has a
collection of books in it, doesn't it?)

-Howard
 
V

Victor Bazarov

Wolfram Humann said:
Suppose the user of the class wants to say:
(assuming the book is already in the map, error otherwise)

BOOK stroustrup("The C++ Programming Language");
cout << "Stroustrup has " << stroustrup.pages << " pages" << endl;

or I have

LIBRARY::addBook(BOOK book);

myLibrary.addBook("The C++ Programming Language");

I already found that here I need a BOOK::BOOK(const char* title_) (with
char* instead of string) constructor. I think the reason is that otherwise
I have an implicit conversion chain with two user-defined conversions...?

Doesn't convince me. If you need to find out whether it's there or not,
use 'find'. If it's there, you will get it, if it's not, you can either
rely on an exception thrown, or a NULL pointer return value. If you know
it's not there, construct by giving all needed arguments. If you know
that it's there, no need to construct another object.

Victor
 
W

Wolfram Humann

Doesn't convince me. If you need to find out whether it's there or not,
use 'find'. If it's there, you will get it, if it's not, you can either
rely on an exception thrown, or a NULL pointer return value. If you know
it's not there, construct by giving all needed arguments. If you know
that it's there, no need to construct another object.

Victor

O.k., I see your point: a constructor should make new objects -- why use
it when the object is already there.

Well, for the users of my class these are like new objects as they have
never seen them. My real case is not about books but an abstraction of
some electronic hardware. The map is pre-filled with the available modules
without interaction of the class's user. Maybe that's a weak argument...

For named objects I think at least a copy-constructor is required, even
when findByTitle is used instead of a constructor:

BOOK tolkien( findByTitle("The Hobbit") );

Also the constructor taking a title is somewhat convenient. Compare:

cout << BOOK("The Hobbit").author;
cout << BOOK::findByTitle("The Hobbit").author;
(Of course .author needs to be replaced by an access function)

Finally for historical reasons I don't have a choice: I have to provide
this constructor to be consistent with existing software, even if it's not
the most logical thing to do.

My question remains: Can I play any (dirty?) tricks to avoid looking up my
map three times. (Knowing that this should not really work I tried
assigning to "this" in the initializer list or calling a copy constructor.
Of course that failed.)

If nothing else helps, I would consider making my members non-const or
putting them all in a struct and initialize that struct in the
constructors initialization list:

BOOK::BOOK(const string& title)
: memberStruct(findByTitle(title).memberStruct)
{}
 
H

Howard

Wolfram Humann said:
O.k., I see your point: a constructor should make new objects -- why use
it when the object is already there.

Well, for the users of my class these are like new objects as they have
never seen them. My real case is not about books but an abstraction of
some electronic hardware. The map is pre-filled with the available modules
without interaction of the class's user. Maybe that's a weak argument...

I'm not even sure what that argument means. What is a "user" of your class?
Another programmer? An end-user? Or code that makes calls to your class's
code? I don't see what having "never seen them" means in relation to any of
those cases. And I still don't see why you need to construct a new instance
of an object if the object already exists, because all that is doing is
making a copy of some existing object, and your limited example of using
that copy does not justify that action.

Plus, you still haven't explained what a LIBRARY is (nor why your collection
of BOOKs is contained in a BOOK object instead of a LIBRARY object).
For named objects I think at least a copy-constructor is required, even
when findByTitle is used instead of a constructor:

BOOK tolkien( findByTitle("The Hobbit") );

I don't understand what you mean when you say "even when findByTitle is used
instead of a constructor". A copy-constructor takes a const reference to an
existing object (a BOOK) and creates a new one based on that. It's
irrelevant how you obtained that reference.
Also the constructor taking a title is somewhat convenient. Compare:

cout << BOOK("The Hobbit").author;
cout << BOOK::findByTitle("The Hobbit").author;

Is findByTitle a static function??? Why are you specifying
BOOK::findByTitle, and not something like someBook.findByTitle? This seems
to point tight back to the use of another class (LIBRARY) to hold the
collection instead of an individual BOOK.
(Of course .author needs to be replaced by an access function)

Finally for historical reasons I don't have a choice: I have to provide
this constructor to be consistent with existing software, even if it's not
the most logical thing to do.

Well, the mere existence of a constructor that takes just the title instead
of all its members does not mean that you have to *always* use that
constructor to use such a book. Assuming that findByTitle returns a
reference to an existing BOOK object (does it?), then you might want to do
this:

BOOK& someOtherBook = someBook.findByTitle("The Hobbit");
cout << someOtherBook.author << endl;
cout << someOtherBook.price << endl;

There's no construction going on here at all, and I only need the
findByTitle call once.

(Or if findByTitle returns a pointer, then use a pointer instead of a
reference...but my point is the same.)
My question remains: Can I play any (dirty?) tricks to avoid looking up my
map three times. (Knowing that this should not really work I tried
assigning to "this" in the initializer list or calling a copy constructor.
Of course that failed.)

If nothing else helps, I would consider making my members non-const or
putting them all in a struct and initialize that struct in the
constructors initialization list:

BOOK::BOOK(const string& title)
: memberStruct(findByTitle(title).memberStruct)
{}

I think your example is just too abstracted to make any sense as it relates
to the actual problem that you have. But it looks like you've answered your
own question: if you MUST use this constructor and need to initialize it via
findByTitle, then using the result of that call to initialize (construct) a
member struct appears to me to be a good way. But in that case, you'll end
up doing exactly what we've been suggesting: calling findByTitle to get a
pointer or reference to an existing BOOK, and passing that pointer or
reference to a constructor (in this case the constructor of that new struct,
instead of a new BOOK object).


Without a more realistic sample of your code and the problem you face,
(especially showing the context in which you feel the need to use a newly
constructed object like that instead of a pointer or reference), I doubt we
can tell you any more than you already know.

(Sorry if I sound argumentative here....don't mean to.)

-Howard
 
K

Karl Heinz Buchegger

I still believe you have a design flaw.

A single book is not a collection of books.
There should be 2 distinct classes: book - library

You don't ask a book to find some title. You ask a library
to look up if there is a book with a specific title.

Therefore the problem you are now facing simply disappears
if you correct the design.

LIBRARY MyLibrary;

BOOK* pBook = MyLibrary.FindByTitle( "TheHobbit" );

// if the book is in the library, you now have a pointer to it.

if( pBook )
cout << pBook->author;
else {
cout << "I don't have that book. Please provide the details to create it\n";
cin >> author;

MyLibrary.Add( BOOK( "The Hobbit", author ) );
}
 
W

Wolfram Humann

I started writing a reply but can't stay long enough today to finish it.
Hopefully tomorrow will be less busy. Sorry.

Wolfram
 
W

Wolfram Humann

I'm not even sure what that argument means. What is a "user" of your
class?
Another programmer? An end-user? Or code that makes calls to your
class's
code? I don't see what having "never seen them" means in relation to
any of
those cases. And I still don't see why you need to construct a new
instance
of an object if the object already exists, because all that is doing is
making a copy of some existing object, and your limited example of using
that copy does not justify that action.

First of all: I highly appreciate all the efforts (yours and those of
Victor and Karl Heinz) to help me. This discussion provided me with a
number of good points to think about in my design. But I think I see that
my limited example may not be sufficient. And my employer might not
appreciate if I post the original code ;-). I tried to invent a simple
case and now we are more into a discussion of that case's design than into
my original problem. I'll still try to explain what I can. If it's more
confusing than illuminating, just tell me I'm a hopeless case and abort
the discussion...

The "users" are other programmers. At the beginning of their sourcecode
they (naturally) don't have any BOOK objects. But the map of books is
already filled. Maybe they even will not be allowed to add other books
(the constructor taking enough arguments to create a fully defined BOOK
may be private). So how do they make BOOK objects? I proposed a
constructor taking a title that returns a copy of an existing book:

BOOK tolkien("The Hobbit");

If I understand Victor correctly, his point is that a constructor should
create something new and not return something existing. I suppose he would
prefer using a function like findByTitle() that returns an existing book.
If findByTitle() is static, as Victor proposed, that would work for
nameless objects:

cout << BOOK::findByTitle("The Hobbit").author;

But how do I create named objects? I don't expect to have a default
constructor (is there a default book?). So the only way to create a named
object would be to copy-construct from a nameless object returned by
findByTitle():

BOOK tolkien( findByTitle("The Hobbit") );

Plus, you still haven't explained what a LIBRARY is (nor why your
collection
of BOOKs is contained in a BOOK object instead of a LIBRARY object).

O.k., forget about LIBRARY. I invented it quickly and it was a bad
example. Objects of my second class contain a subset of by "books",
LIBRARY would be more like a superset. I'll try to invent something better
a few lines down :)
My collection of BOOKs is NOT contained in any BOOK object. It's static,
so it's contained in the class. I think that's not too bad -- each object
contains a single book, the class has a collection of all my books.

If my second class contains a subset of my books, maybe a good example
could be BOOKSHELF or GENRE. Let's try GENRE. My only point here was, if
GENRE has a method addBook() that takes a BOOK as an argument, then with
the constructor I proposed I could say:

GENRE fantasy;
fantasy.addBook("The Hobbit");

otherwise I have to say

fantasy.addBook( findByTitle("The Hobbit") );

I like that first form, but that's just personal preference.
I don't understand what you mean when you say "even when findByTitle is
used
instead of a constructor". A copy-constructor takes a const reference
to an
existing object (a BOOK) and creates a new one based on that. It's
irrelevant how you obtained that reference.

Victor wrote: "why do I need to construct another object from a single
'title' when I already have one sitting in the map<string, BOOK>?" My
point was: The objects in the map are nameless. If somebody wants a named
BOOK object, he will need some kind of contructor to create it. Either the
contructor taking a title, as I proposed, or a copy constructor as
mentioned above.
Is findByTitle a static function??? Why are you specifying
BOOK::findByTitle, and not something like someBook.findByTitle? This
seems
to point tight back to the use of another class (LIBRARY) to hold the
collection instead of an individual BOOK.

1. findByTitle() wasn't static in my first post. But I think Victor
pointed out correctly that it should be.
2. How do I construct your someBook?
Assuming that findByTitle returns a
reference to an existing BOOK object (does it?), Yes, it does.
then you might want to do this:

BOOK& someOtherBook = someBook.findByTitle("The Hobbit");
cout << someOtherBook.author << endl;
cout << someOtherBook.price << endl;

There's no construction going on here at all, and I only need the
findByTitle call once.

(Or if findByTitle returns a pointer, then use a pointer instead of a
reference...but my point is the same.)

I'm a bit reluctant to use pointers or references to objects in my map. I
don't think that current implementations will ever reallocate memory as
they grow. The nodes of the underlying tree have pointers to parent, left
and right, so it's sufficient to manipulate those pointers during groth.
But is this behavior guaranteed by any standard?

How was someBook created? Maybe like this:

BOOK someBook( findByTitle("Programming Perl") );

Now does it make sense to say:

someBook.findByTitle("The Hobbit"); ?

I would prefer to use findByTitle() as a static function:

BOOK::findByTitle("The Hobbit");
Without a more realistic sample of your code and the problem you face,
(especially showing the context in which you feel the need to use a newly
constructed object like that instead of a pointer or reference), I doubt
we can tell you any more than you already know.

O.k.. I just thought that I might have overseen some very simple solution.

Again, thank you for this discussion. If forced me to think again about
some aspects of my design and that's always good!

Wolfram
 
V

Victor Bazarov

Wolfram Humann said:
[...] At the beginning of their sourcecode
they (naturally) don't have any BOOK objects. But the map of books is
already filled. [...]

Here is what I fail to understand. What is the map filled with
if there are no BOOK objects? If you can answer that, you can
probably give a better description of your problem. Try.

Victor
 
W

Wolfram Humann

Wolfram Humann said:
[...] At the beginning of their sourcecode
they (naturally) don't have any BOOK objects. But the map of books is
already filled. [...]

Here is what I fail to understand. What is the map filled with
if there are no BOOK objects? If you can answer that, you can
probably give a better description of your problem. Try.

Victor

How about this case: There's a static member

BOOK::defineBook(const string& title, const string& author,
const int pages, const double price);

It is usually called at the beginning of the source-code. It uses a
(private?) constructor to create BOOK objects and puts them into the map.
So the objets are there but how can I access them? The key of the map is
the title, so I think access should be by title. Suppose I know there is a
BOOK in the map titled "The Hobbit". How do I create a BOOK object
"tolkien" that refers to that book? My idea was:

BOOK tolkien("The Hobbit");

That requires a constructor taking a title that retrieves books form the
map.

O.K., I tried. Is it any clearer now? :)

Wolfram
 
H

Howard

I still don't think that there is any reason to construct a new object at
all, unless you are adding it to your map. If you're just inquiring for
information about an existing book, then simply return a reference to one.
If there is a chance that the book might not exist, then either throw an
exception and handle it, or else return a pointer instead of a reference,
and make it nil when the specified book doesn't exist. Regardless of
whether you're getting a "named" variable or not, that's just the logical
way of doing it. For example,:

(using a returned reference, and trapping for the exception:)

try
{
cout << BOOK::findByTitle("The Hobbit").author;
cout << BOOL::findByTitle("The Hobbit").publisher;
}
catch(...)
{
// ...error? create and add a new one?
}

(very inefficient, since it looks up the book twice, but still there is no
constructor being used),

OR (better, in my opinion, using a returned pointer which *may* be nil)...

BOOK* someBook = BOOK:findByTitle("The Hobbit");
if (someBook)
{
cout << someBook->author;
cout << someBook->publisher;
}
else
{
// ...error? create and add a new one?
}

This second design is just much more logical. But notice that in neither
case is there any construction going on. If you need to add a book that is
NOT in the map, then you'll already have all that info at hand, and can
simply call the constructor that takes the whole set of required information
as parameters, with no need to look up whether a book exists or not during
construction, because you've already checked fact that via the static
findByTitle function.

Hope this helps..

-Howard
 
K

Karl Heinz Buchegger

Wolfram said:
Wolfram Humann said:
[...] At the beginning of their sourcecode
they (naturally) don't have any BOOK objects. But the map of books is
already filled. [...]

Here is what I fail to understand. What is the map filled with
if there are no BOOK objects? If you can answer that, you can
probably give a better description of your problem. Try.

Victor

How about this case: There's a static member

BOOK::defineBook(const string& title, const string& author,
const int pages, const double price);

It is usually called at the beginning of the source-code. It uses a
(private?) constructor to create BOOK objects and puts them into the map.
So the objets are there but how can I access them? The key of the map is
the title, so I think access should be by title. Suppose I know there is a
BOOK in the map titled "The Hobbit". How do I create a BOOK object
"tolkien" that refers to that book? My idea was:

BOOK tolkien("The Hobbit");

That requires a constructor taking a title that retrieves books form the
map.

O.K., I tried. Is it any clearer now? :)

You still have not figured out, that you have a design error.
A single book is not a library of books. Nor does one single
book contain the whole library.

There should be 2 classes:
BOOK and LIBRARY
 
W

Wolfram Humann

If you're just inquiring for information about an existing book,
then simply return a reference to one [...], or else return a
pointer instead of a reference, and make it nil when the
specified book doesn't exist.

I see your point and I agree that this is a clean solution. Two remarks:

1. I still could not find any statement in the standard that
references/pointers to map entries will always remain valid when the map
grows/shrinks. As I said, I don't think the current implementation of a
map will reallocate memory (like e.g. the vector does) but I don't want to
rely on this unless the standard defines that it's illegal for a map to
reallocate memory for its entries.

2. Using references or pointes may be more logical than creating
"redundant" objects but at least for my current project I won't be able to
do this. My code has to remain consistent with existing code.

Wolfram
 
H

Howard

Wolfram Humann said:
If you're just inquiring for information about an existing book,
then simply return a reference to one [...], or else return a
pointer instead of a reference, and make it nil when the
specified book doesn't exist.

I see your point and I agree that this is a clean solution. Two remarks:

1. I still could not find any statement in the standard that
references/pointers to map entries will always remain valid when the map
grows/shrinks. As I said, I don't think the current implementation of a
map will reallocate memory (like e.g. the vector does) but I don't want to
rely on this unless the standard defines that it's illegal for a map to
reallocate memory for its entries.

2. Using references or pointes may be more logical than creating
"redundant" objects but at least for my current project I won't be able to
do this. My code has to remain consistent with existing code.

Wolfram

Well, then just write a proper copy-constructor. Let findByTitle return a
const reference (throwing an exception if not found), and pass its result as
the parameter for the constructor, like this:

try
{
BOOK aBook(findByTitle("The Hobbit"));
}
catch(...)
{
//...error?
}

or

try
{
const BOOK& refBook = BOOK::findByTitle("The Hobbit");
BOOK* pPook = new BOOK(refBook);
}
catch(...)
{
// ...error?
}

-Howard
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,767
Messages
2,569,572
Members
45,045
Latest member
DRCM

Latest Threads

Top