Suppose I have a class that has a member reference
variable, and sometimes I want to initialize it,
and sometimes I don't. How much sense does it make
to initialize it to a dummy member variable (to shut
the compiler up)?
As a concrete example, let's say I have a class that
is constructed with a value of or reference to some kind
of handle, but the handle might be a number or it might
be a string:
struct Handle {
Handle (int iHandle) :
iHandle_(iHandle),
sHandle_(dummy),
useIHandle_(true)
{}
Handle (const std::string& sHandle) :
iHandle_(0),
sHandle_(sHandle),
useIHandle_(false)
{}
const int iHandle_;
const std::string& sHandle_;
bool useIHandle_;
std::string dummy_;
}
The point is that the member reference variable sHandle_
is supposed to be initialized (in the constructors'
initialization lists), whether or not it's actually
going to be used. The constructor that takes an int
argument doesn't have any std::strings floating
around with which to initialize sHandle_, hence the
introduction of the member variable dummy_.
Is this a reasonable approach? Is there an established
idiom for doing this kind of thing?
First, when you're using your object in string mode, where are your letters
being stored?! You're NOT storing them at all; you're just re-using the data
from the source object. When that object dies, your letters die and you have
a dangling reference! That's bad, and you should store the string by value,
just like the integer. (And you can't tell what the remote's lifetime is; it
could be a global that outlasts your object, or a temporary that'll die
soon.[1])
Second, why is your primary data const? Do you actually need to override the
default const/volatile status for you sub-objects? (Said default is to match
the const/volatile state of the enclosing object.) Said overrides mess up
some automatic actions, and making all your external methods const serves the
same purpose.
For your main problem: you need to use either an integer or a string. And
neither both together nor none at all; always exactly one. If you're using
C++11, this is a job for a union.
// Not tested
class Handle {
union MyData {
int i;
std::string s;
MyData( int i = 0 ) : i{ i } {}
MyData( std::string const &s ) : s{ s } {}
MyData( std::string &&s ) : s{ std::move(s) } {}
} data;
bool useStr;
public:
Handle( int i ) : data{ i }, useStr{ false } {}
Handle( std::string const &s ) : data{ s }, useStr{ true } {}
Handle( std::string &&s ) : data{ std::move(s) }, useStr{ true } {}
Handle( Handle const &that );
Handle( Handle &&that );
~Handle();
void swap( Handle &that );
Handle & operator =( Handle that )
{ this->swap(that); }
//...
};
In previous versions of C++, a union could not have a member if any of its
special member functions were non-trivial. All of std::string's s.m.f.s are
non-trivial, so you have to use one of the aforementioned solutions if you're
using a pre-11 compiler. For C++11, non-trivial types can be in a union, but
each special member function is deleted (i.e. cancelled) unless all its
members' versions of that s.m.f. is trivial. Handle will start off with NO
s.m.f.s available until we add them in manually.
By default, using a union member in a class's initializer list will initialize
its first member. Lexically-later members cannot be initialized this way. An
empty initializer will do default initialization on said first member, but that
can't happen here since default initialization is cancelled for this union
(since std::string's version is non-trivial). The way around that is to add
constructors to the union. This idiom (tagged union) usually has the union
member's type anonymous, but I have to name the type in order to add
constructors. I can initialize the union with an integer, a string via copy,
or a string via move. The integer constructor also acts as a default one;
I'll explain that later.
My copy of your constructors just forward to the union's version. The
string-move constructor works similarly. All of them set the Boolean flag for
integer versus string.
I don't know how reference members work with copy-constructors, but your
Handle's version should work if references are supported. For my version, we
copy the flag and manually copy the data.
// Not tested
Handle::Handle( Handle const &that )
: useStr{ that.useStr }
{
if ( useStr )
new ( &data.s ) decltype( data.s ){ that.data.s };
else
data.i = that.data.i;
}
We can't copy the data member within the member initialization list, since
there's two choices and the decision is run-time based. That's why I added a
default-constructor to the union type. Since the union is set to be an integer
by default, I can do a simple assignment when the source object is in integer
mode. When the source is in string mode, I allocate the string's
copy-constructor call in place with placement-new. (Technically, I should have
called the "int" destructor for that memory segment first, but I can get away
without doing it because "int" has a trivial destructor.)
// Not tested
Handle::Handle( Handle &&that )
: useStr{ that.useStr }
{
if ( useStr )
new ( &data.s ) decltype( data.s ){ std::move(that.data.s) };
else
data.i = std::move( that.data.i );
}
The move constructor is similar. I couldn't do it at all if I left the primary
data const....
// Not tested
Handle::~Handle()
{
if ( useStr )
data.s.~decltype( data.s );
else
data.i.~decltype( data.i );
}
In the destructor, manually call the current object's constructor. I'm not
sure that using a "decltype" expression is legal as a form of a destructor
(name), or if it's legal but not supported by today's compilers. You could
leave off the "else" clause, since that member's destructor is trivial.
void Handle::swap( Handle &that );
I punted implementing (proper) copy-assignment and move-assignment operators by
doing a by-value (copy) assignment and swap. I'm also too tired to write out
swap. The swap function holds the real action. Do a four-way if-else chain.
If you have two integers or strings, call the standard swap routine. If you
have a mix, copy both the integer and string, destruct both, and
placement-allocate the new sub-objects. Since "int" is trivial, you can skip
some steps. However, it's nice to make swapping never-throw if possible, so
wrap the allocations in try-catch and mind your action order. (Actually, you
can skip wrapping the "int" constructor. And if you're super paranoid, you
could wrap the string destructor.)
Daryle W.
[1] It'll still outlast your object if the latter is a temporary too.