Type Traits Issue List

Doc.no.:J16/ 03-0102=WG21/N1519
Date: 18 Sept 2003
Project: Programming Language C++
Subgroup: Library
Wiki: http://www.research.att.com/~ark/cgi-bin/lwg-wiki/wiki?RegularExpressions
Reply to: John Maddock <john@johnmaddock.co.uk>

This document describes open issues in the type-traits proposal (N1424).

Use of Language in type transformations

Pete Becker writes: I've come across two places in the type traits proposal where the words don't seem to properly express the intent. They're both in TT.4, Transformations between Types:

For "Array Modifications," the text says: type: defined to be a type that is the same as T, except any top level array bounds have been removed. Technically, an array bound (see 8.3.4) is the constant-expression that specifies the number of elements in the array, so for 'typedef int arr[10]', the type of arr without the array bound is 'int []'. From the code preceding this text, it looks like that's not what's intended: the result should be 'int'. The way to say this is: for a type 'array of T', the resulting type is T; for any other type U, the resulting type is U. This also suggests that the name isn't right. Perhaps element_type? For an array, that's what the standard calls it.

The other one comes immediately after that. It's the description of the effect of add_pointer: type: defined to be a type that is the result of the expression &t, where t is an imaginary lvalue of type T. What about a type T that defines an operator&? According to this text, the effect of add_pointer is to get the return type of operator&. The code, on the other hand (and again I think the code expresses the actual intent) is remove_bounds<remove_reference<T>::type>::type*, that is, produce a pointer to the underlying type. I don't have suggested standardese here. It might be worthwhile to define a few terms that can be used to talk about these things. Something on the line of "underlying type" would be useful for this description, as well as for things like add_cv. (That would also eliminate the undefined term "imaginary lvalue" from this definition).

John Maddock notes:

·        There was some discussion on the list as to what remove_bounds should be called, there seemed to be no better alternative than remove_dimension.

·        There is an additional issue with remove_bounds – the code shown does not function correctly for types “array of unknown bound of T”, this is easily fixed though.

Proposed changes:

Replace all occurrences of “remove_bounds” with “remove_dimension”.

Replace:

template <class T> struct remove_bounds{

   typedef T type;

};

template <class T, std::size_t N> struct remove_bounds<T[N]>{

   typedef T type;

};

type: defined to be a type that is the same as T, except any top level array bounds have been removed.

With:

template <class T> struct remove_dimension{

   typedef T type;

};

template <class T, std::size_t N> struct remove_dimension<T[N]>{

   typedef T type;

};
template <class T> struct remove_dimension<T[]>{
   typedefs T type;
};

type: for a type 'array of U', the resulting type is U; for any other type V, the resulting type is V.

[Note – for multidimensional arrays, only the first array dimension is removed – end note]

[example

// the following assertions should all hold:
      assert((is_same<remove_dimension<int>::type, int>::value));
      assert((is_same<remove_dimension<int[2]>::type, int>::value));
      assert((is_same<remove_dimension<int[2][3]>::type, int[3]>::value));
      assert((is_same<remove_dimension<int[][3]>::type, int[3]>::value));

- end example]

Replace:

type: defined to be a type that is the result of the expression &t, where t is an imaginary lvalue of type T.

With:

type: defined to be a type that is the same as remove_reference<T>::type* if T is a reference type, otherwise T*.

Why three headers?

Pete Becker writes: Three headers seems excessive. Why not put them all into <type_traits>? That would simplify things for users, who wouldn't have to remember which of the three headers defines the template they're interested in. Currently, <type_traits> has 33 templates (not counting helpers), <type_compare> has 3, and <type_transform> has 11. The classification is reasonable in itself, but I don't think it's particularly helpful.

A number of people expressed support for one header on the LWG reflector – the changes needed are mostly administrative and/or organizational

Proposed resolution: Combine the three type traits headers into a single header named <type_traits>.

Is integral_constant an implementation detail?

Pete Becker writes: The type_traits templates need two types to distinguish between true and false. These are named true_type and false_type. The paper says that these two types are required to be instances of the template integral_constant, specifically integral_constant<bool, true> and integral_constant<bool, false>, respectively. Is this a legitimate requirement, or is it an implementation detail? Do we lose anything by making the details of these types up to the implementation, leaving it open to the implementation to use, for example, a pair of ordinary classes instead of further burdening template instantiation?

John Maddock replies: It was intended to be a requirement: there are some function overload situations where you do actually need to refer to integral_constant rather than just the typedefs, in fact there is one in the rationale:

template<typename T> 
T* do_copy(const T* first, const T* last, T* out, const true_type&)
{ /* optimised copy here */ }
 
template<typename I1, typename I2, bool b>
I2 do_copy(I1 first, I1 last, I2 out, const integral_constant<bool, b>&)
{ /* non-optimised copy here */ }
 
template<typename I1 typename I2>
inline I2 copy(I1 first, I1 last, I2 out)
{
   typedef typename std::iterator_traits<I1>::value_type value_type;
   return do_copy(first, last, out, has_trivial_assign<value_type>()); 
} 

Note that in the second overload for do_copy using false_type in place of integral_constant<bool, something> wouldn't actually work, if one had to rely on the typedefs alone one would have to add a third overload.

Perhaps a better example is for implementing user defined traits classes:

template <class T> 
struct my_trait : public integral_constant<
                      bool,
                      some-complex-logical-expression> 
                      {}; 

Or where dispatch to a function overload requires a logical combination of traits:

template <class T> 
void do_fill(T* i, T* j, const T& val, const true_type&) 
{ memset(i, val, j-i); } 
 
template <class I, class T, class U> 
void do_fill(I i, I j, const T& val, const U&) 
{
   while(i != j)
   { *i = val; ++i; } 
} 
 
template <class I, class T,> 
void fill(I i, I j, const T& val) 
{
   typedef typename iterator_traits<I>::value_type out_type;
   do_fill(i, j, integral_constant<
                       bool,
                       sizeof(out_type == 1)
                       && has_trivial_assign<out_type>::value
                       && has_trivial_assign<T>::value
                    >()
                );
}

Basically it comes in the category of "useful helper class", yes you can do without it, but eventually you end up reinventing it if it's not there.

Aleksey Gurtovoy provided other examples, and showed how integral_constant facilitates interaction with Boost’s MPL library (and other libraries that use similar meta-programming techniques).

There followed a fairly long discussion which centered around the following issue: integral_constant is not strictly required to implement the proposal as it stands, however it is useful in extending the proposal, and several people suggested extensions which would in fact require it (alignment_of and variations thereof included in some of the issues below, plus the following change to the Unary Type Traits Requirements).

Revising the Unary Type Traits Requirements

Some of the new traits that have been proposed require that their “value” member is of integral type rather than bool.  The library was always intended to be capable of extension in this way, but the UnaryTypeTrait requirements talk about Boolean properties.

Proposed changes:

Replace table TT2 with:

Table TT2 - UnaryTypeTrait requirements

Expression

Return type

Requirement

X<T>::value_type

An integral type

The type of X<T>::value.

X<T>::value

value_type

An integral constant expression that takes the value of the specified trait.

X<T>::type

integral_constant<value_type,value>

A type for use in situations where a typename is more appropriate than a value. The class template integral_constant is declared in <type_traits>.

X<T>::type t = X<T>()

 

Both lvalues of type X<T> const& and rvalues of type X<T> are implicitly convertible to X<T>::type.

New Unary Type Traits

Several new type traits have been proposed on the –lib reflector list:

alignment_of

template <class T> struct alignment_of {

   static   const std::size_t value                    = implementation_defined;

   typedef  std::size_t                                value_type;

   typedef  integral_constant<value_type,value>        type;

   operator type()const;

};

value: An implementation-defined integer value representing a number of bytes of the alignment of objects of type T; an object of type T may be allocated at an address that is a multiple of it’s alignment (3.9).

Notes: This template has been present in the Boost library for some time and is of proven utility, several users have expressed disappointment that it was not included in the original type traits proposal.  Gabriel Dos Reis has pointed out that some ABI specifications have two kinds of alignment: the alignment of an object within a struct (field alignment), and the ideal alignment that an object would like to take (for example for the starting address of an array of T).  The specification above deals only with the former alignment type, and is based on the wording in 3.9 paragraph 5; there being no other alignment concept in the current standard.   It is unclear whether this is the best name, Gabriel Dos Reis writes: “the template-argument list <T> is already read as `of tee’, the suffix `_of’ is overkill, IMO”, on the other hand, the existing name hasn’t generated comment from Boost users.

has_virtual_destructor

template <class T> struct has_virtual_destructor {

   static   const bool value                     = implementation_defined;

   typedef  bool                                 value_type;

   typedef  integral_constant<bool,value>        type;

   operator type()const;

};

value: true if type T has a virtual destructor (12.4) otherwise false.

Notes: is_polymorphic is sufficient to determine whether a type can be dynamic_cast to another, but doesn’t tell you whether it can be safely cast to a base type and still be safely deleted (since a type can be polymorphic but still not have a virtual destructor); this trait fills that gap.  Note however that the memory management provided by shared_ptr is capable of correctly deleting a shared_ptr<Base> even after a cast from shared_ptr<Derived> irrespective whether Base has a virtual destructor.  Whether this negates the need for this trait is an open issue.  Bronek Kozicki has also asked for is_safely_destructable: this evaluates as true unless a type has a public base class that does not have a virtual destructor; this trait would be used in template factory functions to assert that the type is actually safe to be factory-created and then destroyed via a pointer to base.

A fuller rationale and proposal for has_vitual_destructor and is_safely_destructable can be found in document J16/03-0091 = WG21/N1508.

rank

template <class T> struct rank {

   static   const std::size_t value                    = implementation_defined;

   typedef  std::size_t                                value_type;

   typedef  integral_constant<value_type,value>        type;

   operator type()const;

};

value: An implementation-defined integer value representing the rank of objects of type T (8.3.4). [Note – the term “rank” here is used to describe the number of dimensions of an array type – end note]

[example

// the following assertions should hold:
assert(rank<int>::value == 0);
assert(rank<int[2]>::value == 1);
assert(rank<int[][4]>::value == 2);

- end example]

Notes: There was some discussion about the choice of the term “rank” here – the term is correctly used in this case, but the standard uses “rank” to refer to the shape of an array in 8.3.4.  There is no precedent for this template in the Boost library, although at least one list member had previously independently invented it prior to its discussion.

dimension

template <class T, unsigned I = 0> struct dimension {

   static   const std::size_t value                    = implementation_defined;

   typedef  std::size_t                                value_type;

   typedef  integral_constant<value_type,value>        type;

   operator type()const;

};

value: An implementation-defined integer value representing the dimension of the I’th bound of objects of type T (8.3.4).  If the type T is not an array type, has rank of less than I, or if I == 0 and is of type “array of unknown bound of T”, then value shall evaluate to zero; otherwise value shall evaluate to the number of elements in the I’th array bound of T.  [Note – the term “dimension” here is used to describe the number of elements in an array type – end note]

[example

// the following assertions should hold:
assert(dimension<int>::value == 0);
assert(dimension<int[2]>::value == 2);
assert(dimension<int[2][4]>::value == 2);
assert(dimension<int[][4]>::value == 0);
assert((dimension<int, 1>::value) == 0);
assert((dimension<int[2], 1>::value) == 0);
assert((dimension<int[2][4], 1>::value) == 4);
assert((dimension<int[][4], 1>::value) == 0);

- end example]

Notes: There was some discussion about the choice of the term “dimension” here – the term is often abused within the programming community and “extent” was one alternative suggested.  There is no precedent for this template in the Boost library.  The wording for boundary cases need checking for sensibility: what should happen when T is an unbounded array or is not an array at all?

New transformation traits

The following type transformation traits have been suggested on the –lib reflector:

aligned_storage

template <std::size_t Len, std::size_t Align> struct aligned_storage{

   typedef implementation_defined type;

};

type: an implementation defined POD type with size Len and alignment Align, and suitable for use as uninitialized storage for any object of type T whose size is Len, and alignment Align.

Notes: This template is present in the Boost library as “type_with_alignment”, but that doesn’t accurately describe what it does. 

There is a suggestion that type must be an array of bytes in order to fit within the current language structure, what impact does this have on compiler writers?

Gabriel Dos Reis writes:

When designing a type traits for properly aligned storage, one has to keep in mind the restrictions on object aliasing and interaction with the rest of the language/library.  In particular, the language defines "byte" (i.e. char or unsigned char) as the type of storage. Furthermore, we have the following restrictions: 

3.10/15:

If a program attempts to access the stored value of an object through an lvalue of other than one of the following types the behavior is undefined:

n      the dynamic type of the object,

n      a cv-qualified version of the dynamic type of the object,

n      a type that is the signed or unsigned type corresponding to the dynamic type of the object,

n      a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,

n      an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a sub-aggregate or contained union),

n      a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,

n      a char or unsigned char type.

Rationale: It is tempting to reformulate this template as:

template<class T> struct aligned_storage; // provides storage for type T

However this fails several important use cases:

·        The first case is a discriminated union (see boost::variant - which has aligned_storage<Size, Align> as an implementation detail) where one of a list of types may be constructed in the same raw storage.

·        The second case is the pool allocator in boost/detal/quick_allocator.hpp, where something similar to aligned_storage<Size, Align> is used in order to reuse the pools for types that have equal sizes and alignments but are not the same type, for example int, unsigned int, long, unsigned long.

·        Another example: think of a std::function<> that wants to avoid allocating memory for small objects. It needs to reserve a small buffer - aligned_storage<12, some_suitable_align> - and then check in the

template<class F> function(F f);

constructor whether F can fit in 12 bytes and whether it is compatible with some_suitable_align. F isn't known at the time the function<> is declared, so it can't be used as an argument to aligned_storage.

remove_all_dimensions

template <class T> struct remove_all_dimensions{

   typedef T type;

};

template <class T, std::size_t N> struct remove_all_dimensions<T[N]>{
   typedef typename remove_all_dimensions<T>::type type;
};
template <class T> struct remove_all_dimensions<T[]>{
   typedef typename remove_all_dimensions<T>::type type;
};

type: for a type 'multi-dimensional array of U', the resulting type is U; for any other type V, the resulting type is V.

[example

// the following assertions should all hold:
assert((is_same<remove_all_dimensions<int>::type, int>::value));
assert((is_same<remove_all_dimensions<int[2]>::type, int>::value));
assert((is_same<remove_all_dimensions<int[2][3]>::type, int>::value));
assert((is_same<remove_all_dimensions<int[][3]>::type, int>::value));

- end example]

Howard Hinnant reports that this trait has been useful in implementing is_pod, and that the trait is present in the Metrowerks library, there was some discussion on the list as to whether the trait has uses beyond implementing is_pod though.