Provide a class supporting "do-the-right-thing clause" dispatch.
More...
Namespaces |
namespace | bslmf |
Detailed Description
- Outline
-
-
- Purpose:
- Provide a class supporting "do-the-right-thing clause" dispatch.
-
- Classes:
-
- See also:
- Component bslmf_matchanytype, Component bslstl_deque, Component bslstl_string, Component bslstl_vector
-
- Description:
- This component defines a class,
bslmf::MatchArithmeticType
, to which any arithmetic type can be implicitly converted. A class with that conversion property is useful for meeting the certain requirements of the standard sequential containers (e.g., bsl::vector
, bsl::deque
, bsl::string
).
- Sequential containers have several overloaded method templates that accept a pair of input iterators (e.g., constructors,
insert
and append
methods), but which must not accept arithmetic types (e.g., bool
, char
, short
, int
, double
). See "ISO/IEC 14882:2011 Programming Language C++" (see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf
), "Section 23.2.3 [sequence.reqmts]", paragraphs 14-15. This requirement is informally known as the "do-the-right-thing clause". See http://gcc.gnu.org/onlinedocs/libstdc++/ext/lwg-defects.html#438
.
- The convertibility of arguments to
bslmf::MatchArithmeticType
is used to dispatch calls with arithmetic arguments to the appropriate methods. Note that this technique (a variation of "tag dispatch") can be compromised if one uses a class that defines a conversion operator to bslmf::MatchArithmeticType
(or a conversion operator to bslmf::MatchAnyType
) but otherwise do not behave as arithmetic types.
- Enumerations (not arithmetic types themselves) are implicitly convertible to
blsmf::MatchArithmeticType
, and will dispatch as some integer (a sub-set of arithmetic) type.
-
- Usage:
- In this section we show intended use of this component.
-
- Example 1: "Do-the-Right-Thing" Clause Dispatch:
- Suppose we want to create a container with two constructors:
-
one constructor providing initialization with multiple copies of a single value (a "repeated value constructor"), and
-
the other providing initialization from a sequence of values delimited by a pair of iterators (a "range constructor").
- A naive implementation can result in common usage situations in which arguments meaningful to the former constructor are provided but where the compiler resolves the overload to the latter constructor.
- For example, the
MyProblematicContainer
class outlined below provides two such constructors. Note that each is atypically embellished with a message
parameter, allowing us to trace the call flow.
template <class VALUE_TYPE>
class MyProblematicContainer {
public:
MyProblematicContainer(std::size_t numElements,
const VALUE_TYPE& value,
const char *message);
template <class INPUT_ITER>
MyProblematicContainer(INPUT_ITER first,
INPUT_ITER last,
const char *message);
};
template <class VALUE_TYPE>
MyProblematicContainer<VALUE_TYPE>::MyProblematicContainer(
std::size_t numElements,
const VALUE_TYPE& value,
const char *message)
{
assert(message);
printf("CTOR: repeated value: %s\n", message);
}
template <class VALUE_TYPE>
template <class INPUT_ITER>
MyProblematicContainer<VALUE_TYPE>::MyProblematicContainer(
INPUT_ITER first,
INPUT_ITER last,
const char *message)
{
assert(message);
printf("CTOR: range : %s\n", message);
}
The problem with the MyProblematicContainer
class becomes manifest when we create several objects: const char input[] = "How now brown cow?";
MyProblematicContainer<char> initFromPtrPair(
input,
input + sizeof(input),
"Called with pointer pair.");
MyProblematicContainer<char> initFromIntAndChar(
5,
'A',
"Called with 'int' and 'char'.");
MyProblematicContainer<char> initFromIntAndInt(
5,
65,
"Called with 'int' and 'int'.");
Standard output shows: CTOR: range : Called with pointer pair.
CTOR: repeated value: Called with 'int' and 'char'.
CTOR: range : Called with 'int' and 'int'.
Notice that the range constructor, not the repeated value constructor, is invoked for the creation of initFromIntAndInt
, the third object.
- The range constructor is chosen to resolve that overload because its match of two arguments of the same type (
int
in this usage) without conversion is better than that provided by the repeated value constructor, which requires conversions of two different arguments.
- Note that, in practice, range constructors (expecting iterators) dereference their arguments, and so fail to compile when instantiated with arithmetic types.
- If we are fortunate, range constructor code will fail to compile; otherwise, dereferencing integer values (i.e., using them as pointers) leads to undefined behavior.
- Note that, in many other situations, overloading resolution issues can be avoided by function renaming; however, as these are constructors, we do not have that option.
- Instead, we redesign our class (
MyContainer
is the redesigned class) so that the calls to the range constructor with two int
arguments (or pairs of the same integer types) are routed to the repeated value constructor. The bslmf::MatchArithmeticType
class is used to distinguish between integer types and other types.
- First, we define the
MyContainer
class to have constructors taking the same arguments as the constructors of MyProblematicContainer
:
template <class VALUE_TYPE>
class MyContainer {
public:
MyContainer(std::size_t numElements,
const VALUE_TYPE& value,
const char *message);
template <class INPUT_ITER>
MyContainer(INPUT_ITER first, INPUT_ITER last, const char *message);
};
Then, we isolate the essential actions of our two constructors into two private, non-creator methods. This allows us to achieve the results of either constructor, as appropriate, from the context of the range constructor. The two privateInit*
methods are: private:
void privateInit(std::size_t numElements,
const VALUE_TYPE& value,
const char *message);
template <class INPUT_ITER>
void privateInit(INPUT_ITER first,
INPUT_ITER last,
const char *message);
Note that, as in the constructors of the MyProblematic
class, the privateInit*
methods provide display a message so we can trace the call path.
template <class VALUE_TYPE>
void MyContainer<VALUE_TYPE>::privateInit(std::size_t numElements,
const VALUE_TYPE& value,
const char *message)
{
assert(message);
printf("INIT: repeated value: %s\n", message);
}
template <class VALUE_TYPE>
template <class INPUT_ITER>
void MyContainer<VALUE_TYPE>::privateInit(INPUT_ITER first,
INPUT_ITER last,
const char *message)
{
assert(message);
printf("INIT: range : %s\n", message);
}
Now, we define two overloaded privateInitDispatch
methods, each taking two parameters (the last two) which serve no run-time purpose. As we shall see, they exist only to guide overload resolution at compile-time. Notice that the first overload has strict requirements on the last two arguments, but the second overload (accepting bslmf::MatchAnyType
in those positions) will match all contexts in which the first fails to match.
- Then, we implement the two
privateInitDispatch
overloads so that each invokes a different overload of the privateInit
methods:
-
The
privateInit
corresponding to repeated value constructor is invoked from the "strict" overload of privateInitDispatch
.
-
The
privateInit
for range construction is invoked from the other privateInitDispatch
overload.
template <class VALUE_TYPE>
template <class INTEGER_TYPE>
void MyContainer<VALUE_TYPE>::privateInitDispatch(
INTEGER_TYPE numElements,
INTEGER_TYPE value,
const char *message,
bslmf::MatchArithmeticType ,
bslmf::Nil )
{
(void) message;
privateInit(static_cast<std::size_t>(numElements),
static_cast<VALUE_TYPE>(value),
"Called via 'privateInitDispatch'.");
}
template <class VALUE_TYPE>
template <class INPUT_ITER>
void MyContainer<VALUE_TYPE>::privateInitDispatch(
INPUT_ITER first,
INPUT_ITER last,
const char *message,
bslmf::MatchAnyType ,
bslmf::MatchAnyType )
{
privateInit(first, last, message);
}
Next, we use overloaded privateInitDispatch
method in the range constructor of MyContainer
. Note that we always supply a bslmf::Nil
object (an exact type match) as the final argument, the choice of overload will be governed according to the type of first
. Consequently, if first
is implicitly convertible to bslmf::MatchArithmeticType
, then the overload leading to repeated value construction is used; otherwise, the overload leading to range construction is used. template <class VALUE_TYPE>
template <class INPUT_ITER>
MyContainer<VALUE_TYPE>::MyContainer(INPUT_ITER first,
INPUT_ITER last,
const char *message)
{
privateInitDispatch(first, last, message, first, bslmf::Nil());
}
Notice that this design is safe for iterators that themselves happen to have a conversion to int
. Such types would require two user-defined conversions, which are disallowed by the C++ compiler, to match the bslmf::MatchArithmeticType
parameter of the "strict" privateInitDispatch
overload.
- Then, we implement the repeated value constructor using a direct call to the repeated value private initializer:
template <class VALUE_TYPE>
MyContainer<VALUE_TYPE>::MyContainer(std::size_t numElements,
const VALUE_TYPE& value,
const char *message)
{
privateInit(numElements, value, message);
}
Finally, we create three objects of MyContainer
, using the same arguments as we used for the three MyProblematicContainer
objects. const char input[] = "How now brown cow?";
MyContainer<char> initFromPtrPair(input,
input + sizeof(input),
"Called with pointer pair.");
MyContainer<char> initFromIntAndChar(5,
'A',
"Called with 'int' and 'char'.");
MyContainer<char> initFromIntAndInt(5,
65,
"Called with 'int' and 'int'.");
Standard output shows: INIT: range : Called with pointer pair.
INIT: repeated value: Called with 'int' and 'char'.
INIT: repeated value: Called via 'privateInitDispatch'.
Notice that the repeated value privateInit
method is called directly for the second object, but called via privateInitDispatch
for the third object.