This page describes what should be considered when creating a value type. It provides a quick checklist which can be used as a reminder of the various considerations. It also contains an explanation of the why the various consideration are important and possible implementatation strategies.
There are many places where allocators are mentioned. The allocators
mention on this page are generally refering to the base class
bslma::Allocator
as used by BDE.
There are also some components from BDE used as examples or to
simplify some processing.
Operation | Mandatory | Likely | Maybe |
---|---|---|---|
resource management | yes | ||
allocator-awareness | conditional | ||
polymorphic base | no | ||
support inheritance | no | ||
default constructor | yes | ||
copy constructor | yes | ||
move constructor | yes | ||
copy assignment | yes | ||
move assignment | yes | ||
other constructors | yes | ||
destructor | yes | ||
swap() |
yes | ||
equality operators | yes | ||
relational operator | yes | ||
output operator | yes | ||
hashAppend() |
yes | ||
bool conversion |
yes | ||
tuple -like access |
yes | ||
pre/post conditions | yes |
Value types are types which can be copied with the two copies behaving identical under normal conditions when the same operations are applied (when functions exit abnormally, e.g., using an exception to indicate failure to allocate a resource, the behavior may be different). The following operations and guidelines should be considered when defining value types:
- Resource Management: it is mandatory to correctly manage all resources and to document whether an object or one of its members may contain non-memory resources.
- Allocator Awareness: mandatory for any type which [potentially] contains allocator-aware members.
- Polymorphic Base: it is generally not a good idea to have value types implement a base class.
- Support Inheritance: value types generally don't make good base classes.
- Default Constructor: optional but required for regular types and BDE value semantic types.
- Copy Constructor: mandatory -- needed for the definition of value types. The compiler generated version may be sufficient.
- Move Constructor: optional but a good idea if there are [potentially] non-trivial members.
- Copy Assignment: mandatory -- needed for the definition of value types but the compiler generated version may be sufficient.
- Move Assignment: optional but a good idea if there are [potentially] non-trivial members.
- Other Constructors: there are probably
other constructors and, at least, one argument constructors
should probably be
explicit
. - Destructor: mandatory but the compiler generated version may be sufficient.
swap()
functions: optional but a good idea if there are [potentially] non-trivial members.- [in]equality operators: mandatory; needed for the definition of value types. It should compare all salient attributes.
- relational operators: optional but a good idea if the value type as some form of an order.
- output operator: optional but a good idea and required for BDE full value semantic types.
hashAppend()
: optional but a good idea for any type which may be a viable key in an unordered container.bool
Conversion: optional but a good idea for any type which has a reasonable interpretation as abool
.tuple
-like access: optional but a good idea for any type which is just a collection of attributes.- Pre/Post Ponditions: when defining a value type which has pre- or post-conditions consider suitable assertions in strategic positions
The examples on this page often use member functions defined in the
class definition. This approach is used only as a short-hand. A
real implementation would probably implement the members in a
suitable translation unit or for cases where they should be inline
as functions made explicitly inline
after the class definition.
A type is a value type if objects of the type can be copied and
copies of the entity behave semantically exactly the same under
normal conditions. The objects can be tested using equality operations
which compare the salient attributes. Specifically, if f()
is an
operation mutating an object of a value type Value
, the following
holds:
void g(Value v0) {
Value v1(v0);
assert(v0 == v1);
f(v0);
f(v1);
assert(v0 == v1);
}
The behavior may not necessarily be exactly identical as non-salient
attributes may affect how the entity is represented. For example,
a std::vector<T>
may have a large capacity making it unnecessary
to relocate elements when appending new elements. The capacity is,
however, not copied, i.e., the copy may need to relocate the objects
held internally. These differences shall not affect the semantics
of the operations.
If any of the operations fails due to program state independent of the object's value, the condition may also fail to hold. For example, if there are insufficient resources available resource allocation may succeed for one object but not the other.
This section explains the considerations and outlines recommended implementation approaches for the various operations.
Resource management is crucial for long-running or resource-hungry applications. Thus, any object needs to make sure that it can release all resources it is managing.
Specifically for memory a subsystem may employ a memory management strategy consisting of simply reusing memory without destroying objects because it is known the objects won't be used. This will, however, result in resource leaks for all non-memory resources. To avoid such resource leaks it is important to document if an object or one of its subobjects may manage any non-memory resource.
For value type it is rare to manage non-memory resources. Resources are generally non-copyable and value types need to be copyable. However, some value types may share, e.g., a connections to a database employing reference counting.
The first thing to consider is whether the type should be allocator aware. Allocator awareness permeates all of the structural members of a class. It also inhibits use of having the compiler generate default versions of some members. This section discusses only the immediate implications of allocator awareness and leaves some of the details to later sections discussing individual members.
A class shall be allocator aware if at least one of two conditions hold:
- If the class directly allocates any memory it needs to use an allocator to do so and, thus, it is allocator aware.
- If the class contains an allocator aware object as a member, it needs to be allocator aware itself.
For library components it is important that they accurately follow this model: if a class deviates from this choreography a system using the library component may fail in rather interesting ways. For example, a system may end up leaking memory if it uses an allocator which releases memory in bulk without actually calling destructors.
For allocator aware types a few strict rules apply:
-
The type explicitly declares that it is allocator aware. An easy way to do so is to have the definition contain a corresponding traits declaration:
#include <bslmf_nested_traitdeclaration.h> #include <bslma_usesbslmaallocator.h> class Value { ... public: BSLMF_NESTED_TRAIT_DECLARATION(Value, bslma::UsesBslmaAllocator); ... };
-
The allocator is set during construction and doesn't change after having been set under any condition. The idea is that an object and, where applicable, its subobjects are allocated into a memory arena which keeps being used for the life-time of the object. Changing the allocator would imply that the arena where subobjects are located changes. As a result it is necessary to pay attention to the automatically generated assignment operators to avoid having them change the allocator.
-
The member functions of a class may need to use the allocator after the object has been constructed. If there is an allocator-aware member it may expose an interface to get the allocator and instead of redundantly storing the allocator this interface can be used. An allocator-aware object should probably also expose the allocator itself for use by objects containing the object itself. Normally, the
allocator()
function is used to expose the allocator an object, e.g.:class Value { bdlc::PackedIntArray d_array; ... public: bslma::Allocator* allocator() const { return d_array.allocator(); } ... };
The classes mimicking classes in the standard C++ library use the interface of the standard classes which is
get_allocator()
returning a type-specific interface to allocation. When the default allocatorbsl::allocator<T>
is used, themechanism()
member can be used to get hold of anbslma::Allocator*
:class Value { bsl::string d_text; public: bslma::Allocator* allocator() const { return d_text.get_allocator().mechanism(); } ... };
The descriptions below assume that an allocator-aware class has a member
allocator()
yielding the appropriatebslma::Allocator*
to be used. -
If the object holds an allocator explicitly because there is no member which exposes an allocator it may be reasonable to define it as
bslma::Allocator *const d_allocator_p;
Using a
const
member prevents changing thed_allocator_p
member. It also prevents automatic generation of the assignment operators which is desirable for the purpose of maintenance of the allocator but is possibly not desirable otherwise for classes using multiple members.If the allocator is explicitly held by a member for later use by memory allocations, it also needs to deal with the default value: if the argument is
0
the default allocator needs to be used. The typical implementation approach is to usebslma::Default::allocator()
to potentially obtain the default allocator:Value::Value(bslma::Allocator *allocator) : d_allocator_p(bslma::Default::allocator(allocator)) { }
Both of the concerns of assignment and potentially obtaining the default value are taken care of by
batma::Allocator
which is intended to be used a member of a class. For example:class Value { public: batma::Allocator allocator; ... explicit Value(bslma::Allocator *allocator): d_allocator(allocator) {} ... };
Using the
batma::Allocator
member as apublic
data member directly provides access to the stored allocator. Sincebatma::Allocator
objects are immutable and access to the stored allocator should be provided anyway there isn't any concern about making the data memberpublic
. Of course, if an object wants to observe accesses to the allocator it can use aprivate
data member and provide a suitable accessor function. -
For all constructors taking an allocator as argument the allocator has to be propagated to all subobjects, i.e., all base objects and members, which are allocator aware. For types which directly contain a subobject involving a template argument which may or may not be allocator aware it is typically easiest to wrap the subobject using
bslalg::ConstructorProxy<T>
. For exampletemplate <typename T> class Value { bsl::string d_string; bslalg::ConstructorProxy<T> d_arg; ... public: explicit Value(bslma::Allocator* allocator) : d_string(allocator) // always needs to be forward , d_arg(allocator) { // ConstructorProxy takes care of forwarding if T is allocator aware } ... };
bde_verify
can be used
to verify some of the requirements imposed by allocator awareness.
Value types are generally bad candidates for implementing a protocol. Thus, they should not implement polymorphic basic classes.
A value type may have non-polymorphic base classes. For example,
the most likely representation of a tuple<T...>
is to derive from
a non-polymorphic base for each of the element of T...
. Similarly,
some traits can be implemented by inheriting from a suitable
base.
Value types are normally processed using concrete types. As such,
they are not particular useful as bases classes. Correspondingly,
they shouldn't define any protocol, i.e., they don't have any
virtual
functions. In particular, the destructor is non-virtual
.
It is generally desirable to have a default constructor for value
types. For an accessible default constructor is needed to create
built-in arrays of a type or to create an object which is later set
to a specific value, e.g., when accessing an element in a std::map<K, V>
using the subscript operator. Where the value type has an obvious
default value, a default constructor should be provided. For example,
for number types the value 0
is a reasonably choice as is the
empty state for typical containers.
Not all value types have a reasonable default. For example, there is no one good value for a date type. In case there is no good value there are a few choices how to deal with the default constructor:
-
Have the default constructor not make any guarantee except that the object can be assigned. The implication is that it is possible to create arrays or to define an object which is later assigned to, e.g., getting different values depending on some conditions. The object itself isn't useful for any purpose until it is assigned. This weak form is all what is required for regular types.
Although this may sound relatively useless, this model is implemented by the built-in types! A default constructed object of a built-in type has no defined value and reading it before assigning to it has undefined behavior. The built-in types do offer zero-initialization by value initializing the object: when using
T()
orT{}
the object ends up zero-initialized. Support of this distinction can be achieved when no constructor needs to be declared. With C++11 this semantic can be achieved by defining the default constructor using= default
. -
Despite no good default value being available, some more or less random value can be chosen for the default. This is the choice used for value semantic types. With this approach default constructed objects do get a value which is, however, rarely useful and the types end up being used similar to types using the previous approach, i.e., the value is assigned before it is actually used. The net-effect is that some initialization is done which is never really used.
-
It may be reasonable to not define a default constructor. The immediate implication is that the value type won't be usable as the element of a built-in array without initializing all elements. Likewise, any object created needs to be initialized with some parameter. However, this approach normally doesn't really lead to problems.
For allocator-aware types the default constructor is generally implemented in a form optionally accepting an allocator, i.e.:
class Value {
...
public:
explicit Value(bslma::Allocator *allocator = 0);
...
};
The copy constructor of value semantic types is essential as this constructor allows creation of a copy which behaves the same as the original. The copy constructor needs to establish an object with the same value for all salient attributes as the original: the post condition of the copy operation is that the source is unchanged and the new value compares equal to the source.
For value types it is unusual to have a requirements which somehow
depend on the memory location. As a result, simply copying the the
original members is typically sufficient. With pre-C++11 compilers
the copy constructor would simply not be defined at all and the
compiler would generate it. With C++11 compilers it may be desirable
to spell the copy constructor out explicitly and = default
it
although this will inhibit automatic generation of move operations.
Sadly, allocator-aware types need to be able to set an allocator
on construction. As a result the copy constructor is typically
implemented to take a defaulted allocator. The resulting constructor
cannot be = default
ed but needs to be manually implemented. If
there are many members doing so can become quite error-prone (but
then, having many members is probably a sign that some refactoring
is needed anyway).
If there are non-salient attrributes these can have different values than the original. In particular, if an original object hold an allocator, the allocator is not propagated to the copied object (which implies that the used allocator is never a salient attribute).
For allocator-aware types the copy constructor is generally implemented in a form optionally accepting an allocator. If that's done it needs to pass the optionally specified allocator to all allocator-aware member, e.g.:
class Value {
bsl::string str;
...
public:
Value(Value const& other, bslma::Allocator *allocator = 0)
: str(other.str, allocator) {
}
...
};
The move constructor is optional for value types. If an object of a value type is to be moved and there is no move construtor it will be copied. However, when the object has a non-trivial state it is often more effective to transfer the state to a new object rather than copying it if the original object won't be used again.
Since the state is transferred, a move constructor does propagate the allocator unless an allocator is explicitly specified. As a result it is necessary to be careful when moving an object which uses a local allocator (or any other resource, really) as it needs to be guaranteed that the allocator stays valid as long as the object state is being used. For example
bsl::string do_something() {
char buffer[100];
StackAllocator allocator(buffer);
bsl::string value(&allocator);
// ...
return value; // ERROR: value will be moved using a local buffer
}
To fix the previous example move construction (and copy-elision)
need to be inhibited. An easy way to do so is not to return the
value
using its name. A strict reading of the standard requires
that (value)
inhibits both move construction and copy elision
from value
. It may be advisable to use a call to an identity()
function, though:
template <typename T>
T& identity(T& value) {
return value;
}
Although move constructors get language support only with C++11 it
is possible to support explicit moving with pre-C++ implementations!
To do so, the argument for the move constructor is specified as
bslmf::MovableRef<Value>
, indicating that the source object isn't
needed anymore and its state can be transferred:
class Value {
...
public:
Value(bslmf::MovableRef<Value> other);
...
};
Each member of a class an be movable, not movable but copyable, or an entity which needs to be transferred. The first two cases could be treated identical although it may be more effective to treat them separately. Here is an example implementation of a move constructor showing all three cases:
Value::Value(bslmf::MovableRef<Value> other)
: movable(bslmf::MovableRefUtil::move(bslmf::MovableUtil::access(other).movable))
, copyable(bslmf::MovableRefUtil::access(other).copyable)
, resource(bslmf::MovableRefUtil::access(other).resource) {
bslmf::MovableRefUtil::access(other).resource.reset();
}
This implementation warrants some explanation:
- With C++03
MovableRef<Value>
is an object containing an object of typeValue
. To get hold of this object either the [implicit] conversion fromother
toValue&
can be used or it can be accessed usingbslmf::MovableRefUtil::access(other)
. With C++11other
is an rvalue reference to the original object and there is no need to useaccess(other)
. This indirection is only needed when the code should also work with C++03. - Even when
other
is an rvalue reference it is an lvalue. Thus, the object or any subobject can't bind to an rvalue reference and it can't be moved implicitly. To get themovable
member moved it is necessary to turn the lvalue into something which indicates it can be moved usingbslmf::MovableRefUtil::move()
. - A member which can't be moved but is copyable can be passed on
directly although it can also be
move()
ed: it will be copied whether it is seen as an rvalue reference (or aMovableRef<T>
) or not. - A member which is itself a handle to a resource will first be copied
or moved as any other copyable entity. Once it is moved, the
original needs to be reset (in the example by calling
reset()
but it could, of course, be something different like assigning0
to a pointer) to avoid having the entity destroyed in both the new object and the original.
Subobjects contained in another object should have the same allocator as the containing object. To support move construction of an allocator aware object, an allocator is explicitly specified in the constructor and the state can only be conditionally transferred depending on whether the allocators compare equal: if they do, the state can be transferred otherwise the state needs to be copied. Since this operation is quite different from the use of unconditionally transferring the state it is probably implemented as a separate constructor (there isn't much practical experience with corresponding types, yet, to tell for sure how the typical implementation pattern looks like).
For more information on movable types see the Two Daemons article in Overload 128.
The copy assignment is mandatory for value types and needs to create a copy comparing equal to the argument. There are multiple approaches to implement a copy assignment and which one to choose depends on the situation:
-
Many value types can get away with using the default implementation or with C++11 use an explicitly defaulted (
= default
) version. This approach assumes that all members of the value type are value types.Note that this approach does not yield a strongly exception safe implementation if assignment of any but the first member may throw an exception. The problem is that a thrown exception won't restore the original state if it directly modified the state as the default generated assignment operator does.
A defaulted assignment operator isn't available if any of the members is a reference type or declared
const
as should be done withbslma::Allocator*
members. On the other hand, explicitly dealing with an allocator implies that memory is directly allocated in which case the generated copy constructor probably doesn't quite work. -
An easy approach to leverage other functions (copy constructor, destructor, and
swap()
) is to implement the assignment operator by first copying the argument and then swapping the result into place, and having the destructor of a suitable temporary argument take care of releasing the original representation. This approach yields a strongly exception safe implementation. For example:Value& Value::operator= (Value other) { other.swap(*this); return *this; }
Note that
other
is passed by value. Doing so automatically creates a copy and may even leverge copy elision if a temporary is passed as argument potentially avoiding the neede to copy the object.Sadly, this implementation isn't allocator aware! For an allocator aware implementation a similar approach can be used but it won't be able to leverage copy elision:
Value& Value::operator= (Value const& other) { Value(other, this->allocator()).swap(*this); return *this; }
-
Since assigning to an object using the
swap()
approach always changes the memory this approach isn't the most efficient approach when there are lots of assignments to the same object. When assigning repeatedly to an object which retains the already allocated memory the access to memory can faster because potentially already cached memory is accessed.For classes expecting a lot of assignments it may be reasonable to specifically craft a corresponding assignment operator. Hopefully corresponding classes are rare and already taken care of by a lower-level library. For example,
bsl::vector<T>
andbsl::string
will probably have custom assignment operator implementations.
The default generated move assignment (assuming C++11 support) may just work. However, similar to the copy constructor it may not be strongly exception safe: if the assigned object and the source have different allocators the representation may still need to be copied which may result in an exception being thrown. Also, automatic generation of move assignment isn't available without a C++11 compilers.
The approach to the move assignment is to swap()
the representation
if the allocators agree and otherwise to dispatch to the copy
assignment rather than replicating copy assignment logic. For
example:
Value& Value::operator= (bslmf::MovableRef other) {
Value& other_object = bslmf::MovableRefUtil::access(other);
if (this->allocator() == other_object.allocator()) {
other.swap(*this);
}
else {
*this = other_object;
}
return *this;
}
Most value types probably have other constructors than those treated as special by the language. There are a few basic considerations.
-
Generally, a value type's constructor should initialize all the object's member to an appropriate value. There are a few rare exceptions which closely resemble built-in types. If a type leaves member uninitialized it shouldn't have any constructors at all for C++03 or an explicitly defaulted default constructor (
Value() = default;
) for C++11: this way the user of the type has the opportunity to cause value initialization of the members. -
Constructors taking exactly one argument with C++03 or any argument with C++11 should probably be declared to be
explicit
unless there is a good reason to allow implicit conversions. In most cases implict conversions aren't desirable. -
Allocator-aware types should probably provide a version allowing to pass an allocator for each available constructor. The easiest way to do so is probably to provide a defaulted allocator argument for each constructor. Conventionally the allocator arguments goes last which isn't a good option if the constructor takes a variable number of arguments (with variadic arguments the [optional] allocator best goes first).
-
It is normally preferable to do as much initialization as is possible in the member initializer list rather than default constructing the members and initializing them in the body of the class. Note that built-in types, including arrays, can be value initialized which creates a suitable null value for each built-in element from the member initializer list, e.g.:
class Value { int d_array[10]; public: Value(): d_array() {} ... };
The destructor generally just needs to be publicly accessible. If there are any resources held by the value type which are not automatically released the destructor, obviously, these need to be released. If there are non-memory resources which are released by the destructor this fact should be clearly documented to avoid simply letting go of objects in conjunction with an allocator which recycles memory.
Even if the destructor can be generated automatically it may be
desirable to explicitly define the destructor (possibly using C++11's
= default
) to avoid making the destructor implicitly inline
:
if the class is sufficiently big even the automatically generated
destructor may be sufficiently large.
Swapping two objects of a value type should generally be an efficient operations. When the values are small the default approach to swapping the values may be sufficent:
template <typename T>
void swap(T& value0, T& value1) /* throw-spec elided */ {
T tmp(std::move(value0));
value0 = std::move(value1);
value1 = std::move(tmp);
}
There are a few complications, though:
- With C++03 the default implementation of
swap()
actually copies the values rather than moving them. As a result the operation tends to be rather expensive if there are allocations involved. - For allocator-aware types the above implementation might do even more copies as the temporary uses a default allocator.
In case the type needs to do something interesting on copy/move
construction or on copy/move assignment it is normally best to
define a custom swap()
operation in the same namespace as the
value type which delegates to a member swap()
which does the
actual work, e.g.:
class Value {
bsl::string d_text;
int d_value;
public:
void swap_with_same_allocator(Value& other) {
using bsl::swap;
swap(d_text, other.d_text);
swap(d_value, other.d_value);
}
void swap(Value& other) {
if (allocator() == other.allocator()) {
swap_with_same_allocator(other);
}
else {
Value tmp0(*this, other.allocator());
Value tmp1(other, this->allocator());
other.swap_with_same_allocator(tmp0);
this->swap_with_same_allocator(tmp1);
}
}
...
};
void swap(Value& value0, Value& value1) {
value0.swap(value1);
}
Note that the member-wise swap()
(in swap_with_same_allocator()
)
uses a bit of an awkward dance to make sure the appropriate version
of swap()
is found: the using declaration is used to guarantee
that there is a default version of swap()
found in case there
isn't a better version found using ADL.
It seems the normal requirements on swap()
with respect to the
allocators being supported are somewhat confused:
- The non-member
swap()
generally seems to require that allocators are identical. - The member
swap()
seems to support non-equal allocators.
Dealing nicely with a reasonably generic approach still requires a bit of research...
The equality operator needs to compare all salient attributes of a value type. The set of attributes being compared effectively defines what the value type represents, i.e., what attributes are compared exactly will depend on the value type.
The equality operator needs to define an equivalence relation:
a == a
(reflexive)- if
a == b
istrue
thenb == a
is alsotrue
(symmetric) - if
a == b
andb == c
aretrue
thena == c
is alsotrue
(transitiv)
There are two options on how the equality comparison can be implemented on a technical level:
- The operator is implemented as a member of the type. The main implication is that an implicit conversion is supported on the right hand argument but not on the left hand argument.
- The operator is implemented as a non-member of the type. The
main implication is that it doesn't have access to any
private
attributes of the type unless it is made afriend
but implicit conversions are supported for both arguments.
The equality operator should be complemented by a corresponding
inequality operator. An easy approach is to leverage a generic
implementation of the operators located via ADL and calling, e.g.,
an
equalTo()
member:
class Value
: private batgen::EqualTo<Value> {
public:
bool equalTo(Value const& other) const;
...
};
The private
inheritance from batgen::EqualTo<Value>
causes the
compiler to look for a suitable equality and inequality operators
in a context taking friend
functions define by batgen::EqualTo<Value>
into account. There are suitable operators defined by this class
which call the
equalTo()
member:
namespace batgen {
template <typename Type>
class EqualTo {
friend bool
operator== (Type const& value0, Type const& value1) {
return value0.equalTo(value1);
}
friend bool
operator!= (Type const& value0, Type const& value1) {
return !(value0 == value1);
}
}
For another way to support the equality operators see
tuple
-like access below.
When using a value type is the key for one of the ordered containers
it is easiest if the value type supports the relational operators.
Strictly speaking only the less-than operator<()
is required but
if this operator is defined the other three, i.e., operator>()
,
operator<=()
, and operator>=()
, should also be defined.
The less-than operator should define a strict weak order. That is the following conditions should hold:
a < a
isfalse
for all valuesa
(irreflexive)- if
x < y
istrue
the conditiony < x
is nottrue
(asymmetry) - if
x < y
andy < z
aretrue
the conditionx < z
is alsotrue
(transitivity) - if
x
is incomparable withy
andy
is incomparable withz
thenx
is also incomparable withz
(transitivity of incomparability).x
andy
are incomparable if neitherx < y
nory < x
aretrue
.
Similar to the equality operator the relation operators are best
implemented as non-member to take advantage of potential implicit
conversions on the argument. An easy approach is to define a
member
lessThan()
and have it used by operators defined via a suitable class:
class Value
: private batgen::LessThan<Value> {
public
bool lessThan(Value const& other) const;
...
};
For another way to support the relational operators see
tuple
-like access below.
Most value types should have an output operator creating a suitable, human-readable representation of the object. Although it is desirable that objects which can be formatted can also be read, doing so is generally non-trival and typically ends up not working.
The output operator needs to live in the same namespace as the value type so it can always be found. When instantiating templates these operators are looked up only using argument dependent look-up (ADL).
Typically, the output operators just formats the salient attributes but the details of the attributes may be affected by non-salient attributes. A typical implementation of an output operators could look like this:
template <typename T>
class Value {
int d_member1;
T d_member2;
public:
...
std::ostream& print(std::ostream& out) const {
return out << '[' << this->d_member1 << ", "
<< this->d_member2 << ']';
}
};
std::ostream& operator<< (std::ostream& out, Value const& value) {
return value.print(out);
}
For another way to support output operators see
tuple
-like access below.
Many value types can be used as keys in any of the unordered containers.
To do so a type needs to provide a good and fast hash function. For
a hash function h()
it is necessary that for values v1
and v2
with v1 == v2
it follows that h(v1) == h(v2)
. Ideally the
converse should also be true but in general there may be values
v3
and v4
with v3 != v4
but h(v3) == h(v4)
, i.e., there may
be hash collisions. The expecation is that there are only few hash
collisions.
The best approach for supporting hash function is to provide a
hashAppend()
function for the value type. The hashAppend()
function will call hashAppend()
with the salient attributes.
Non-salient attribute need to be excluded when calling hashAppend()
as they may have different values between different objects that must
compare equal. For example
template <typename T>
class Value {
int d_member1;
T d_member2;
public:
...
template <typename Algorithm>
void appendHash(Algoritm& algo) const {
using bslh::hashAppend;
hashAppend(algo, this->d_member1);
hashAppend(algo, this->d_member2);
}
};
template <typename Algorithm, typename T>
void hashAppend(Algorithm& algo, Value<T> const& value) {
value.appendHash(algo);
}
The hashAppend()
function for a value type should be defined in
the same namespace as the corresponding value type as it is found
via argument dependent look-up. When there is a hashAppend()
function for a value type T
it will be used by bsl::hash<T>
.
For another way to create hashing support see
tuple
-like access below.
For some value types it is reasonably to interpret the value as
Boolean value. For example, a smart pointer could reasonably convert
to true
or false
depending on whether it points to something.
While conversions to bool
are useful in Boolean contexts like a
conditional statement, implicit conversions to bool
might easily
end up being used in contexts asking for an integral. With C++11
the easy approach is to make the conversion operator explicit
:
class Value {
...
public:
explicit operator bool();
...
};
With C++03 conversion operators, unlike conversion constructors,
can't be made explicit
. The conventional approach for C++03 is
to not convert to bool
but rather to a member function pointer
type: member function pointers can still be used in conditional
expressions but they won't convert to anything useful otherwise.
A relatively easy approach to use a suitable type for Boolean
conversion operators is to use bsls::UnspecifiedBool<Value>
to
deal with most of the awkwardness
class Value {
...
public:
typedef bsls::UnspecifiedBool<Value>::BoolType BoolType;
operator BoolType() const {
bool result = ...;
return bsls::UnspecifiedBool<Value>::makeValue(result);
}
...
};
Often value types are essentially collections of attributes. When
a value can be reasonably seen as a sequence of attributes it makes
sense to expose a corresponding view and take advantage of generic
functions leveraging it. Providing tuple
-like access makes the
attributes accessible in a common way. The idea is that algorithms
can use access functions to provide functionality without much
boiler plate.
Sadly, with C++03 there is no really nice way to create the necessary information. Even with C++11 and C++14 still lacking reflection support the needed declarations are somewhat repetitive. On the other hand, out of simple declarations some useful functionality can be generated.
For example, here is a way to create a value type equipped with equality operators, relational operators, output operator, and hashing support using a few simple declarations:
#include "bat/gen/tuple.h"
#include "bat/gen/tuplevalue.h"
class Value
: private batgen::tuple_value<Value>
{
bool bv;
int iv;
char cv;
public:
typedef batgen::tuple_members<
batgen::tuple_const_member<bool, Value, &Value::bv>,
batgen::tuple_const_member<int, Value, &Value::iv>,
batgen::tuple_const_member<char, Value, &Value::cv>
> tuple;
Value(bool bv, int iv, char cv) : bv(bv), iv(iv), cv(cv) {}
};
The magic is in the declaration of the member type tuple
: it
declares the necessary information needed to provide access using
operations which are similar to those used by std::tuple<...>
(sadly, they can't be identical beause the use of explicit template
arguments for the index inhibits argument dependent look-up).
Where functions have preconditions they should seek to verify these preconditions. Clearly, not all preconditions can reasonably be checked but in many cases it is viable to check preconditions.
The cost of checking preconditions varies widely. As a result some preconditions can always be checked, even in an optimized build. Other preconditions would only be checked during development when finding interface misuses has a priority. Still other preconditions would only be checked when suspecting an actualy issue because it is fairly expensive to have them checked.
One way to cater for the different needs is to have different
assertion macros and use them according to the importance and cost
of the respective assertion. The compoment
bsls_assert
provides one such set of macros:
BSLS_ASSERT_OPT(condition)
is used for assertions which should be done even in optimized code. For example, a check whether a pointer is non-NULL
may be such a condition.BSLS_ASSERT(condition)
is used for typical assertions with reasonable cost. They would be used during normal development but should not be enabled for an optimized build for example it could be used to determine if two containers have a common size.BSLS_ASSERT_SAFE(condition)
is used for checks which should be enabled only for a safe build. For example, it could check the precondition that a sequence is sorted which may cause the check to dominate the time complexity of an operation which is otherwise efficient.
In no case should any of the assertions have any side effect! It has to be assumed that all of the assertions have an indeterminate state for a build. In particular, it needs to be viable that all of the assertions are disabled.
In
Hashing support
"... Ideally the opposite should also be ..."
do you want to say
"... Ideally the converse should also be ... "
also ".. between different objects compare equal .." -> " between different objects that must compare equal "