Archive for April, 2006

Associated Type Deduction

April 28, 2006

Associated types are extremely important for expressing the capabilities of intermediate types within concepts. For instance, the reference type of an Input Iterator is the return type of the iterator’s operator*; all we know about the reference type is that it is Copy Constructible and can be implicitly converted to the value_type of the iterator (another associated type). Whenever a concept map is defined for a particular concept, definitions need to be given for every associated type of that concept. For instance, consider this Callable2 concept:

auto concept Callable2<typename F, typename X, typename Y> {
  typename result_type;
  result_type operator()(F&, X, Y);
};
        

This concept is marked auto, meaning that the compiler should be able to implicitly generate concept maps when needed. For instance, it should be able to implicitly generate Callable2<int(*)(int, int)>. However, this does not work, because no definition of the associated type result_type is provided! In truth, this is only an annoyance: one can write a concept map that handles every case for binary function pointers:

template<typename R, typename P1, typename P2, typename A1, typename A2>
  where Convertible<A1, P1> && Convertible<A2, P2>
  concept_map Callable2<R (*)(P1, P1), A1, A2> {
    typedef R result_type;
  };
        

This concept map solves the problem (it has been in ConceptGCC for several months). However, the need to specify result_type defeats the purpose of auto. What about user-defined function objects? They will also need their own concept maps, so this problem represents a barrier to adoption, because there are many function objects out in the Real World that will need concept maps.

The solution, which is implemented in ConceptGCC as of today (although it is currently “lightly tested”), is to require that the compiler implicitly generate associated types from the return types of model operations. For instance, when the compiler is attempted to match the type int (*)(int, int) to the Callable2 concept, it needs to determine if it can compile this function:

Callable2<int (*)(int, int)>::result_type
Callable2<int (*)(int, int)>::operator()(int (*&f)(int x, int y) {
  return f(x, y);
}
      

As part of type-checking that function, the compiler computes the type of f(x, y) to determine if it is convertible to the result_type. When result_type has not already been defined by the user, the compiler will implicitly define result_type to the type of f(x, y). Now we can implicitly match Callable2<int (*)(int, int)>: no concept map necessary!

Applying this change throughout the concept descriptions in the ConceptC++ Standard Library allows for greater simplification. For instance, ConceptGCC currently has a Callable2 concept that looks like the above, but a completely separate BinaryPredicate concept that looks like this:

auto concept BinaryPredicate<typename F, typename X, typename Y> {
  bool operator()(F&, X, Y);
};
        

However, what is a BinaryPredicate other than a Callable2 whose result_type is convertible to bool? We can now directly express this relationship through refinement, without worry that users will have to write concept maps for these trivial, structural concepts.

auto concept BinaryPredicate<typename F, typename X, typename Y>
       : Callable2<F, X, Y> {
  where Convertible<result_type, bool>;
};
        

In truth, the ability to determine the values of associated types from return types is not necessarily new to ConceptC++. One could use a default value for the associated type that redirects to decltype:

auto concept Callable2<typename F, typename X, typename Y> {
  typename result_type = decltype((F())(X(), Y()));
  result_type operator()(F&, X, Y);
};
        

Aside from the avoidable problems that F, X, and Y now need default constructors, this formulation is sufficiently confusing: if the user does provide a concept map that overrides the syntax of operator(), which operator() does that decltype expression refer to? One found through Argument Dependent Lookup or the one given in the concept map?

In summary, associated type deduction should simplify the use of concepts by allowed the implicit generation of associated type definitions. This means writing fewer and shorter concept maps, better backward compatibility, and more expressive concepts… as soon as I finish testing the ConceptGCC implementation.

Thanks to Rolf Bonderer, whose examples and request for associated type deduction pushed me to get this implemented in ConceptGCC.

unique_copy is Truly Unique

April 27, 2006

The C++ Standard Library contains an algorithm called unique_copy, which copies the first of each group of unique elemnts from an input sequence to the output sequence. The C++ standard provides the following requirements:

     template<class InputIterator, class OutputIterator>
       OutputIterator
         unique_copy(InputIterator first, InputIterator last,
                     OutputIterator result); 

     template<class InputIterator, class OutputIterator,
              class BinaryPredicate>
       OutputIterator
         unique_copy(InputIterator first, InputIterator last,
                     OutputIterator result, BinaryPredicate pred);
      

Requires: The ranges[first, last) and [result, result+(last-first)) shall not overlap.

Unfortunately, there is a problem: the requires clause does not include the CopyConstructible requirement, without which the algorithm is not implementable. Library Defect 241 fixes this problem, by adding the following to the requiresclause:

The expression *result = *first must be valid. If neither InputIterator nor OutputIterator meets the requirements of forward iterator then the value type of InputIterator must be Copy Constructible. Otherwise Copy Constructible is not required.

These two sentences split the two versions of unique_copy listed into six different implementations: one where we have basic input and output iterators for parameters (which, therefore, requires CopyConstructible), one where the input iterator argument is actually a Forward Iterator, and one where the output iterator argument is actually a mutable Forward Iterator. These three algorithms require different overloads in ConceptGCC, as follows:

template<InputIterator InIter, class OutIter>
  where OutputIterator<OutIter, InIter::value_type> && EqualityComparable<InIter::value_type> &&
        Assignable<InIter::value_type> && CopyConstructible<InIter::value_type> &&
        !ForwardIterator<InIter> && !MutableForwardIterator<OutIter>
  OutIter unique_copy(InIter first, InIter last, OutIter result);

template<ForwardIterator InIter, class OutIter>
  where OutputIterator<OutIter, InIter::value_type> &&
        EqualityComparable<InIter::reference>
  OutIter unique_copy(InIter first, InIter last, OutIter result);

template<InputIterator InIter, MutableForwardIterator OutIter>
  where EqualityComparable<OutIter::reference, InIter::value_type> &&
        Assignable<OutIter::reference, InIter::reference> &&
        !ForwardIterator<InIter>
  OutIter unique_copy(InIter first, InIter last, OutIter result);
    

unique_copy is the first place that we’ve found within the C++ Standard Library that absolutely requires the use of not constraints, which I recently added to ConceptGCC. Not constraints only allow a template to be used when a concept map is not available for a particular concept-id. Without the concept requirements, calling unique_copy with forward iterators would match each of the three unique_copy overloads, but no overload is better than the others: the second and third overloads depend on Forward Iterators (instead of Input and Output Iterators), but they do not require Copy Constructible. By adding the not constraints, we make the first overload disappear when either iterator type is a Forward Iterator, thereby disambiguating all of the calls.

The ABI Killer

April 26, 2006

One open question that has been nagging me for months is whether concepts can be introduced into C++ without breaking the Application Binary Interface (ABI). In particular, can we avoid changing name mangling for templates that use concepts? With our current definition of concepts for C++, we do need to change name mangling to include concepts. The following example illustrates why we need to make this change:

concept A<typename T> {}
concept B<typename T> {}

template<typename T> where A<T>
int foo(T&, int x, int y) { return x+y; } // #1

template<typename T> where B<T>
int foo(T&, int x, int y) { return 2*x + y % 365; } // #2

template<typename T> where A<T> int foo_as_A(T& x) { return foo(x, 5, 17); }
template<typename T> where B<T> int foo_as_B(T& x) { return foo(x, 5, 17); }

concept_map A<int> { };
concept_map B<int> { };

int main()
{
  int x = 5;
  foo(x, 5, 17); // error: ambiguous call to foo()
  foo_as_A(x);   // okay: calls foo #1
  foo_as_B(x);   // okay: calls foo #2
}
        

Essentially, we have two overloads of foo, one which requires a model of concept A (called #1) and the other that requires a model of concept B (called #2). However, we’ve said that int is both an A and a B. If we try to call foo with T=int, both #1 and #2 are equally good matches, resulting in an ambiguity.

However, we can resolve this ambiguity by creating the foo_as_A function, which requires an A. When foo_as_A is type-checked, it’s call to foo is bound to foo #1. Then, when foo_as_A<int> is instantiated, it causes the instantiation foo<int> using #1, and there are no ambiguities. Similarly, the instantiation of foo_as_B<int> causes the instantiation of foo<int>, this time using foo #2. At this point, we have a serious problem: foo<int> is being instantiated twice with the same type parameters and signature, therefore producing two different functions with the exact same mangled name. When compiling this program with ConceptGCC, the result is the following error:

/var/tmp//ccuY2EuL.s:56:FATAL:Symbol __Z3fooIiEiRT_ii already defined.

Having proof that concepts break the ABI is both liberating and troubling. It is liberating in the sense that it opens up the possibility of extensions to concepts that clearly break the ABI, such as Jeremy Siek’s notion of “scoped models”, which are intended to improve modularity for C++ concepts. Of course, it is troubling that concepts will make C++0x programs ABI-incompatible with C++03 programs. This may happen even without concepts (even small language changes tend to break the ABI), but if concepts are the only ABI breaker it may be worth the extra effort to save the ABI. I can think of two ways to make this example ill-formed, thereby “saving” the existing ABI:

  • Change the way instantiation of function calls is handled, so that when instantiating foo_as_A<int>, both foo overloads are considered. Then the foo_as_A trick will become worthless (we’ll get
    an instantiation-time ambiguity), but we’ll have saved the ABI.
  • The nuclear option: Call it a violation of the One Definition Rule (ODR).

Let’s Start with Performance

April 25, 2006

Generic Programming has always been about performance. One should be able to write generic algorithms using natural abstractions and have those generic algorithms compile to efficient, concrete instances. The C++ template mechanism allows us to do this, and so should concepts.

ConceptGCC took a big step in this direction today, now that it properly implements the parameter-passing semantics of the upcoming “joint” concepts proposal. The Indiana proposal used to allow signatures to receive parameters by-value, which implies extra copies that made concept-constrained templates less efficient than unconstrained templates. The “joint” proposal takes signature parameters of type T and transforms them into const T& parameters, eliminating these copies (originally suggested by Alexander Stepanov). The following table provides some timing information comparing the performance of ConceptGCC with and without concepts support enabled. I used the search_n test from the libstdc++ performance testsuite for evaluation purposes.

search_n.cc Concepts No Concepts Relative Performance
Forward Iterator 716 654 Concepts are 9.48% slower
Random Access Iterator 324 324 No difference

In the one case, concepts slow us down by almost 10%; in the other, concepts do not matter at all. While 10% is unacceptable for a production compiler, it is far better than what we had previously: at one point, ConceptGCC with concepts was five times slower than without concepts. The change to pass-by-reference and the move to GCC 4.1 now has us within 10%. I am confident that we will close that gap when we are feature-complete and have time to dedicate to optimization.