Indiana Unversity logo[ConceptGCC]

ConceptGCC :

Refinement: how does ConceptGCC do it, how is it specified, and how should it be

From: Marcin Zalewski (zalewski_at_[hidden])
Date: 2007-11-16 09:50:13


I have been looking at N2421
(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2421.pdf) and
ConceptGCC to try to figure out how do refinement and the related concept maps
work. I found that it was difficult to conclude what the desired behavior
should be in some cases. In particular, I found it hard to interpret the word
"inherit" in the definition of refinement. Below, I list some of the cases
where ConceptGCC differs from the proposed wording. I think that in some of
these cases what ConceptGCC does may be preferable to what the wording
proposes. I would like to hear your feedback on this and tell me if I
misunderstand something and if refinement is crystal clear to you. I separate
the main sections of my post with the '#' character. At the end I very briefly
list a possible conclusions on how things could be done (very much half baked
but I would be interested in elaborating on design choices if someone is willing
to discuss).

# Refinement and name "inheritance"

14.9.3.2 states that concept refinement "inherits" all requirements in the body
of a concept, such that the rquirements of the refining concept are a superset
if the requirements of the refined concept. Consider the following example:

concept A<typename T>
{
  int f();
  int g();
}

concept B<typename T>
: A<T>
{
  float f();
}

struct X {};

concept C<typename T>
: A<T>
{
  X f();
}

According to the concept wording, one could imagine that concept B ends up with
"int f()" and "float f()", which, of course, is not something that should happen
(similarly, concept C ends up with "X f()" and "int f()"). Concept GCC seems to
not "inherit" the signature "float f()" into the concept B somehow noticing that
this would not really work while "int g()" seems to be inherited. I wonder what
should really happen.

So there seem to be at least 2 possibilities:
1. Inherit all names that do not create conflicting overload sets.
2. Inherit all names and if a conflicting overload set is created raise an
error.

ConceptGCC seems to favor the first choice but I think that this is not in
accordance with concept wording proposal which requires "superset" of names in
the inheriting concept. According to the "superset" reasoning, the second
choice seems to be the valid one.

# Lookup of associated functions

14.9.3.1.3 and 14.9.3.1.4 seem to suggest that functions are looked up in
refined concepts. 14.9.3.1.5 also says that if two signatures end up being the
same, only the one found first in a depth-first-search of the refinement
hierarchy is retained. The lookup rules seem to conflict with 14.9.3.2 which
states that names are "inherited" into refining concepts. For example, consider
the code:

concept A<typename T>
{
  void f();
}

concept B<typename T>
: A<T>
{
  void f(int);
}

According to 14.9.3.2, it seems that concept B inherits "void f()" from A and
that there are two functions in B: "void B<T>::f()" and "void B<T>::f(int)".
Thus, the functions in the refined concepts never get taken into account. With
addition of 14.9.3.2.4 (more about it later) this does not make a difference but
is a bit misleading since all refined functions are found in B. Concept GCC
seems to take the view where overload set includes only functions in the current
concept. The only exception is related to the previous point on the refinement,
where signatures that create conflicting overload sets are not inherited in
refinement.

# Implicit concept maps for refined concepts

14.9.3.2.1 states that a concept map for a refining concept implicitly generates
concept map for the refined concept. This does not really work in ConceptGCC.
Take this example:

concept A<typename T>
{
    int f();
    int g();
}

struct X {};

concept Test<typename T>
: A<T>
{
    X f();
}

// Without this concept map, ConceptGCC gives an error when it tries to use "X
// f()" from the concept map for Test to satisfy "int f()" from A.
concept_map A<int>
{
    int f() { return 1; }
    int g() { return 1; }
}

concept_map Test<int>
{
    // The following definition cannot be provided in this concept map:
    // int f() { return 1; }
    X f() { return X(); }
    int g() { return 1; }
}

The concept map for Test fails to generate a concept map for A because
ConceptGCC allows one to introduce an f in Test with a different signature than
f in A. What should be the correct behavior? The way that ConceptGCC goes
about that seems reasonable to me. I can still use int where A or Test is
required but f has a different signature in both cases. This breaks 14.9.3.2,
i.e., the implicit concept map for A cannot be generated.

14.9.3.2.4 requires that concept maps must be "compatible". For example, it is
required that the marked lines in the following code be an error:

concept C<typename T>
{
    typename assoc;
    assoc f(T);
}

concept D<typename T> : C<T>
{
    int g(T);
}

concept E<typename T> : D<T> {}

concept_map C<int>
{
    typedef int assoc;
    int f(int const& x) { return x; }
}

concept_map D<int>
{
    typedef int assoc;
    int g(int const& x) { return -x; }

    // N2421 says that f definition does not to be here
    assoc f(int i) { return i; }
}

concept_map E<int>
{
    typedef float assoc; // error
    int g(int const& x) { return x; } // error

    // N2421 says that f definition does not to be here
    assoc f(int i) { return i; }
}

Yet, ConceptGCC allows such code. Is it a possibility that such solution is
better? 14.9.3.2.4 also states that if the lack of compatibility occurs in
different translation units, no diagnostics have to be issued.

# What if...

What if we could change the concept wording to match the behavior of concepts in
ConceptGCC? I think that some of the (intended or unintended) choices that
ConceptGCC makes may be actually worth considering.

Refinement definition could be simplified:
1. Refining concept inherits all the syntactic symbols of the refined concepts:
1a. Associated types of the refined concept are included in the refining
concept in a _union_ with the associated types that the refining concept defines
(union takes care of duplicates).
1b. Associated functions of the refined concepts are included in the refining
concept unless they introduce overload errors. If two, or more, functions from
the refined concepts introduce an overload conflict together but not separately,
then ambiguity error is raised.
1c. If the above procedure introduces associated functions and types with same
names, an error is raised.

Basically, this means that a refining concept gets all the names from the
refining concept but the names in the refining concepts are preserved in their
own scope. So, for the code:

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

B gives the following names:
A<T>::assoc
B<T>::assoc

Furthermore, as long as possible the compiler will try to include names of
functions from the refined concepts. If these names break overload resolution,
that is ok, and the functions will not be included in the refining concept (as
long as this decision is not ambiguous).

The definition of concept maps for refined concepts could just allow the refined
concepts have different definitions for some names than the refining concepts.
In the last code snippet, A and B had their own assoc each, and it could be that
A<int>::assoc is float while B<int>::assoc is double. And since ConceptGCC
already allows that, it should not be very difficult to implement.