Archive for June, 2006

Type-Checking Default Arguments

June 5, 2006

While working on a simple vector implementation today, I happened across an interesting question regarding constrained templates and default arguments. The code in question is a vector constructor:

template<CopyConstructible T>
class vector {
public:
  typedef size_t size_type;
  typedef T      value_type;
  explicit vector(size_type n, const value_type& x = value_type()) { ... }
};
      

Much to my surprise, the constructor failed to type-check. The error states that value_type has no default constructor, hence the default argument is ill-formed. In the current language, there is already a workaround for this problem: we can split the constructor into two versions, eliminating the default argument entirely. A single-argument version, which requires default-constructibility, and a two-argument version, which does not:

where DefaultConstructible<value_type>
  explicit vector(size_type n) { ... }

vector(size_type n, const value_type&) { ... }
      

Still, we need to consider whether this is the right behavior for default arguments. Should they be type-checked when they are defined or when (and if) they are used? C++ already gives us some guidance, because default arguments in templates are only instantiated (and, therefore, type-checked) when they are needed. Bjarne Stroustrup addressed this particular issue for C++ (without concepts) in committee paper N1070, which describes the reasons why default arguments should be type-checked only when they are used. We adopt the same rule with concepts, so that default arguments are only type-checked at the call site when the caller does not provide an argument for the parameter. So, our original code now type-checks: if a user tries to call this constructor with a non-DefaultConstructible type and without providing a value for x, ConceptGCC will produce an error at the call site.

Thanks to Jaakko Järvi for helping me puzzle through the ramifications of late type-checking of default arguments.

Parameterized Types-of-Types

June 2, 2006

ConceptC++ has long had a shortcut for specifying the constraints on a template type parameter by a single-type concept. Instead of writing such a requirement in a where clause, e.g.,

template<typename Iter> where InputIterator<Iter>
InputIterator<Iter>::difference_type distance(Iter first, Iter last);
      

one can instead write the template header as if InputIterator were the “type” of Iter, e.g.,

template<InputIterator Iter>
InputIterator<Iter>::difference_type distance(Iter first, Iter last);
      

This shortcut syntax actually adds one new feature that simplifies the use of associated types. When declaring a template type parameter as InputIterator Iter, all of the associated types of InputIterator also become available as nested types of Iter. Thus, for instance, Iter::value_type becomes an alias of InputIterator<Iter>::value_type, allowing further simplification of our distance() function:

template<InputIterator Iter>
Iter::difference_type distance(Iter first, Iter last);
      

Although this shortcut has been available for single-type concepts for quite awhile, it was only recently that we considered making them available for multi-type concepts. For me, the use of the shortcut went from excessive to crucial once I realized that we could eliminate a great deal of redundancy in certain where clauses due to the injection of associated types. For instance, here is the declaration of inner_product without using the shortcut anywhere:

  template<typename Iter1, typename Iter2, typename T,
           typename BinaryOperation1, typename BinaryOperation2>
    where InputIterator<Iter1> && InputIterator<Iter2> &&
          Callable2<BinaryOperation2,
                    InputIterator<Iter1>::reference,
                    InputIterator<Iter2>::reference> &&
          Callable2<
            BinaryOperation1,
            T,
            Callable2<
              BinaryOperation2,
              InputIterator<Iter1>::reference,
              InputIterator<Iter2>::reference
            >::result_type
          > &&
          Assignable<
            T,
            Callable2<
              BinaryOperation1,
              T,
              Callable2<
                BinaryOperation2,
                InputIterator<Iter1>::reference,
                InputIterator<Iter2>::reference
              >::result_type
            >::result_type
           >
    T
    inner_product(Iter1 first1, Iter1 last1,
                  Iter2 first2, T init,
                  BinaryOperation1 binary_op1,
                  BinaryOperation2 binary_op2)
      

Yikes! Using the shortcut for single-type concepts leads to some improvement in the where clause:

  template<InputIterator Iter1, InputIterator Iter2, typename T,
           typename BinaryOperation1, typename BinaryOperation2>
    where Callable2<BinaryOperation2, Iter1::reference, Iter2::reference> &&
          Callable2<
            BinaryOperation1,
            T,
            Callable2<BinaryOperation2, Iter1::reference, Iter2::reference>::result_type
          > &&
          Assignable<
            T,
            Callable2<
              BinaryOperation1,
              T,
              Callable2<BinaryOperation2, Iter1::reference, Iter2::reference>::result_type
            >::result_type
           >
    T
    inner_product(Iter1 first1, Iter1 last1,
                  Iter2 first2, T init,
                  BinaryOperation1 binary_op1,
                  BinaryOperation2 binary_op2)
      

Finally, using the shortcut for multi-type concepts we can simplify a bit further:

  template<InputIterator Iter1, InputIterator Iter2, typename T,
           typename BinaryOperation1,
           Callable2<Iter1::reference, Iter2::reference> BinaryOperation2>
    where Callable2<BinaryOperation1, T, BinaryOperation2::result_type>&&
          Assignable<
            T,
            Callable2<BinaryOperation1, T, BinaryOperation2::result_type>::result_type
          >
    T
    inner_product(Iter1 first1, Iter1 last1,
                  Iter2 first2, T init,
                  BinaryOperation1 binary_op1,
                  BinaryOperation2 binary_op2)
      

With templates from the Standard Library, we are not allowed to reorder the template parameters because it would break backward compatibility. If we could reorder the template parameters, however, one could swap BinaryOperation1 and BinaryOperation2 in the template parameter list, so that the remaining Callable2 requirement in the where clause could be moved into the template header. Still, even by shuffling the template parameters, we cannot eliminate the need for the where clause entirely: there are too many interdependencies among the template parameters. So, while extending the “type-of-type” syntax can help us drastically shorten the statement of requirements for a template, it cannot eliminate the need for where clauses entirely.

The latest development version of ConceptGCC supports the “type-of-type” shortcut for multi-type concepts. The implementation of this feature was trivial, posing no interesting challenges. An audit of the algorithms in Chapter 25 of the C++ Standard showed that use of this feature shortened and simplified concept constraints for quite a few algorithms, particularly those that relied on the OutputIterator, BinaryPredicate, and Callable2 concepts.