Skip to content

Instantly share code, notes, and snippets.

@dietmarkuehl
Last active July 4, 2017 03:19
Show Gist options
  • Save dietmarkuehl/74693bb2658ebeda8faa to your computer and use it in GitHub Desktop.
Save dietmarkuehl/74693bb2658ebeda8faa to your computer and use it in GitHub Desktop.
Value Type Checklist

Value Types

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.

Checklist

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

Overview

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:

  1. 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.
  2. Allocator Awareness: mandatory for any type which [potentially] contains allocator-aware members.
  3. Polymorphic Base: it is generally not a good idea to have value types implement a base class.
  4. Support Inheritance: value types generally don't make good base classes.
  5. Default Constructor: optional but required for regular types and BDE value semantic types.
  6. Copy Constructor: mandatory -- needed for the definition of value types. The compiler generated version may be sufficient.
  7. Move Constructor: optional but a good idea if there are [potentially] non-trivial members.
  8. Copy Assignment: mandatory -- needed for the definition of value types but the compiler generated version may be sufficient.
  9. Move Assignment: optional but a good idea if there are [potentially] non-trivial members.
  10. Other Constructors: there are probably other constructors and, at least, one argument constructors should probably be explicit.
  11. Destructor: mandatory but the compiler generated version may be sufficient.
  12. swap() functions: optional but a good idea if there are [potentially] non-trivial members.
  13. [in]equality operators: mandatory; needed for the definition of value types. It should compare all salient attributes.
  14. relational operators: optional but a good idea if the value type as some form of an order.
  15. output operator: optional but a good idea and required for BDE full value semantic types.
  16. hashAppend(): optional but a good idea for any type which may be a viable key in an unordered container.
  17. bool Conversion: optional but a good idea for any type which has a reasonable interpretation as a bool.
  18. tuple-like access: optional but a good idea for any type which is just a collection of attributes.
  19. 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.

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.

Details

This section explains the considerations and outlines recommended implementation approaches for the various operations.

Resource Management

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.

Allocator Awareness

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:

  1. If the class directly allocates any memory it needs to use an allocator to do so and, thus, it is allocator aware.
  2. 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:

  1. 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);
        ...
    };
  2. 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.

  3. 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 allocator bsl::allocator<T> is used, the mechanism() member can be used to get hold of an bslma::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 appropriate bslma::Allocator* to be used.

  4. 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 the d_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 use bslma::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 a public data member directly provides access to the stored allocator. Since batma::Allocator objects are immutable and access to the stored allocator should be provided anyway there isn't any concern about making the data member public. Of course, if an object wants to observe accesses to the allocator it can use a private data member and provide a suitable accessor function.

  5. 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 example

    template <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.

Polymorphic Base

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.

Support Inheritance

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.

Default Constructor

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:

  1. 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() or T{} 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.

  2. 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.

  3. 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);
    ...
};

Copy Constructor

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 = defaulted 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) {
    }
    ...
};

Move Constructor

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:

  1. With C++03 MovableRef<Value> is an object containing an object of type Value. To get hold of this object either the [implicit] conversion from other to Value& can be used or it can be accessed using bslmf::MovableRefUtil::access(other). With C++11 other is an rvalue reference to the original object and there is no need to use access(other). This indirection is only needed when the code should also work with C++03.
  2. 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 the movable member moved it is necessary to turn the lvalue into something which indicates it can be moved using bslmf::MovableRefUtil::move().
  3. 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 a MovableRef<T>) or not.
  4. 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 assigning 0 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.

Copy Assignment

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:

  1. 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 with bslma::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.

  2. 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;
    }
  3. 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> and bsl::string will probably have custom assignment operator implementations.

Move Assignment

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;
}

Other Constructors

Most value types probably have other constructors than those treated as special by the language. There are a few basic considerations.

  1. 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.

  2. 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.

  3. 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).

  4. 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() {}
        ...
    };

Destructor

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.

swap()

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:

  1. 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.
  2. 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:

  1. The non-member swap() generally seems to require that allocators are identical.
  2. The member swap() seems to support non-equal allocators.

Dealing nicely with a reasonably generic approach still requires a bit of research...

Equality Operators

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 is true then b == a is also true (symmetric)
  • if a == b and b == c are true then a == c is also true (transitiv)

There are two options on how the equality comparison can be implemented on a technical level:

  1. 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.
  2. 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 a friend 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.

Relational Operators

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:

  1. a < a is false for all values a (irreflexive)
  2. if x < y is true the condition y < x is not true (asymmetry)
  3. if x < y and y < z are true the condition x < z is also true (transitivity)
  4. if x is incomparable with y and y is incomparable with z then x is also incomparable with z (transitivity of incomparability). x and y are incomparable if neither x < y nor y < x are true.

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.

Output Operator

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.

Hashing Support

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.

bool Conversion

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);
    }
    ...
};

tuple-like Access

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).

Pre/Post Conditions

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.

@patrickmmartin
Copy link

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 "

@tvaneerd
Copy link

tvaneerd commented May 8, 2017

I made some changes to definition of Value type that you might want to consider: https://gist.github.com/tvaneerd/86e199a29f598afa21c6a3b96a668975

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment