Topic: Concepts in C++ ? (Was: Pre-proposal: user-defined error
Author: Christopher Eltschka <celtschk@dollywood.itp.tuwien.ac.at>
Date: Sun, 15 Apr 2001 01:07:12 GMT Raw View
Valentin Bonnard wrote:
>
> > As I wrote, concepts, as I see them, are basically "Meta-Types":
>
> That's exactly what I didn't understoud in your proposal:
> sometimes you wrote
>
> CopyConstructible vector<int>::iterator;
This is outside the concept definition, and declares that
vector<int>::iterator is meant to conform to the concept
CopyConstructible.
>
> and sometimes
>
> CopyConstructible c = CopyConstructible();
This is inside the concept definition and means that a type T
which is CopyConstructible shall allow the construct T c = T();
The point is that the copy-constructible _definition_ can be
translated 1:1 into a template function which uses the concept
name as template parameter. A type conforms to a concept only
if the corresponding template function would compile.
For non-explicit concepts, this is sufficient (an explicit
declaration like the one above is not needed to make a type
conform), while for explicit concepts, you explicitly need a
"conformance statement" like the first one above.
>
> Both is one too much (for my taste).
Well, the point is that the name of the concept is used
differently in concept definitions and outside it. Inside
concept definitions, it is used as placeholder for the real
type conforming to the concept (equivalent to a template
parameter, and following the exact same rules). You could
use another syntax for it (by introducing an additional
formal typename), but IMHO this is the most clear way to
denote that abstract type. But that's just a syntax thing;
the semantics wouldn't be affected by introducing an
additional artificial typename.
Outside the concept definition, the concept name names, of
course, the concept.
>
> For the overloading of templates, I propose that we keep the existing
> functions overloading rules w/ MI.
I think you didn't understand the idea of "template
overloading": It doesn't replace the function overloading, but
is an independent step (which, BTW, would not only be usable on
template functions, but also on template classes).
If you look at STL, you'll see that a big machinery is used
to translate overloading on iterator category (i.e. overloading
on concept) into overloading of functions. This big machinery
is indication for two things: First, the feature is really needed
(you don't get into the trouble to build a big machinery
including the clever use of forwarding functions, traits
and tag types if you don't really need this). Second, it is
not well supported by the language (otherwise the big
machinery would not be necessary to implement such a simple
concept).
Template overloading would add that support to the language,
so the big, complex machinery could be replaced by simple
decclarations, which in addition would naturally be combined
with the correctness tests of the concept extension it builds on.
---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.research.att.com/~austern/csc/faq.html ]
Author: Christopher Eltschka <celtschk@dollywood.itp.tuwien.ac.at>
Date: Tue, 17 Apr 2001 23:10:06 GMT Raw View
Niklas Matthies wrote:
>=20
> On Sun, 15 Apr 2001 01:07:12 GMT, Christopher Eltschka <celtschk@dollyw=
ood.itp.tuwien.ac.at> wrote:
> [=B7=B7=B7]
> > Well, the point is that the name of the concept is used
> > differently in concept definitions and outside it. Inside
> > concept definitions, it is used as placeholder for the real
> > type conforming to the concept (equivalent to a template
> > parameter, and following the exact same rules). You could
> > use another syntax for it (by introducing an additional
> > formal typename), but IMHO this is the most clear way to
> > denote that abstract type. But that's just a syntax thing;
> > the semantics wouldn't be affected by introducing an
> > additional artificial typename.
> >
> > Outside the concept definition, the concept name names, of
> > course, the concept.
>=20
> I think that concepts should not be tied to one type. For example, you
> might want to require a concept on a pair of types. One use of concepts
> is to check requirements on template arguments. It's not hard to imagin=
e
> situations where these requirements do not nicely decompose into
> separate requirements for the individual template arguments, but are
> inherently tied together, and it would be inelegant to have to
> arbitrarily choose one type to express the requirements in terms of thi=
s
> type. For example, when the requirement "T1 can be compared for equalit=
y
> with T2" is needed, is this more a requirement for T1 (parameterized
> with T2) or a requirement for T2 (parameterized with T1)? It would be
> more natural to have requirement on the pair (T1, T2).
That's a good point. "Multi-type concepts" would express
relationships between types, instead of specifyind "meta-types".
This would, of course, need a change in the ovreloading on
concept strategy.
>=20
> On first thought, one very simple way to implement concepts would be to
> allow the keyword `concept' wherever `template' is allowed, and to
> instantiate concepts (where these instantiations result in concept
> checks) the same as templates are instantiated:
>=20
> concept <typename T1, typename T2> // concept definition
> struct EqualityComparable
> {
> static void do_check(const T1& t1, const T2& t2) { t1 =3D=3D t2; =
t2 =3D=3D t1; }
> };
Hmmm... I don't like this. It's much too complicated.
The function metaphor is IMHO much easier. In this case
it would even fit perfectly:
concept<typename T1, typename T2>
EqualityComparableTypes(T1 const& t1, T2 const& t2)
{ // The term "EqualityComparable" is already used for s.th. different
bool b =3D (t1 =3D=3D t2);
bool b =3D (t2 =3D=3D t1);
}
The only difference to real function is the missing function
type.
>=20
> EqualityComparable<int, double> foo; // concept check
What about re-using the explicit template instantiation syntax here?
After all, it _is_ an explicit instantiation of the concept.
concept EqualityComparableTypes<int, double>;
To declare concepts on templates, one then would have to use
(real) templates again:
template<typename T>
concept RandomAccessIterator< vector<T> >;
This could even be extended to constructs like
template<class K, int n: Field<K> >
concept VectorSpace< math::vector<K, n> v>;
// if K describes a field, then math::vector<K, n>
// describes a vector space
>=20
> template <typename T1, typename T2>
> class MyClass
> : EqualityComparable<T1, T2> // concept check
> {
> // or here:
> EqualityComparable<T1, T2> foo; // concept check
> ... actual class declarations ...
> };
What about:
template<typename T1, typename T2 :
EqualityComparableTypes<T1, T2> >
class MyClass { ... };
This syntax has the advantage that it is easily expanded to
functions.
>=20
> void f()
> {
> ...
> int a;
> double b;
> EqualityComparable::do_check(a, b); // concept check
> ...
> }
I don't like this. Concepts apply to types, not to variables.
If _one_ double is comparable to _one_ int, then _all_
doubles are comparable to _all_ ints.
Also, I think concepts are useful basically in templates
(because only those depend on the type). And in templates,
they are part of the interface, not of the implementation
(it's part of the interface that a function takes only
typed which are assignable to each other).
With the syntax I suggested for classes above, one could
also define template functions:
template<typename T1, typename T2 :
EqualityComparableTypes<T1, T2> >
void foo(T1& t1, T2& t2)
{
if (t1 =3D=3D t2)
{
...
}
else
{
...
}
}
>=20
> concept <typename T> // specialization
> struct EqualityComparableWithInt : EqualityComparable<T, int> { };
With my suggested new syntax, the following would work:
concept<typename T : EqualityComparableTypes<T, int> >
EqualityComparableWithInt()
{
// no further conditions
}
>=20
> One could also write:
>=20
> concept <typename T1, typename T2> // concept definition
> void EqualityComparable(const T1& t1, const T2& t2)
> { t1 =3D=3D t2; t2 =3D=3D t1; }
Yes, that's basically my syntax above, except that I didn't
use a return type. OTOH, maybe it would be more consistant to
have a return type.
>=20
> void f()
> {
> ...
> int a;
> double b;
> EqualityComparable(a, b); // concept check
> ...
> }
Same objection as above. I'm strongly agains this "assert-like"
concept conception. In my mind, concepts are about interfaces,
which can - to a certain amount - be checked by the compiler.
In addition, concepts are about types, not about variables.
>=20
> When the compiler fails to compile a concept instantiation, it outputs
> the current concept instantiation path (for nested concept instantiatio=
ns).
> If a concept instantiation compiles successfully, no code or symbol
> table entries are generated for it.
Well, what the compiler outputs, is of course a matter of the
implementation, not one of the language. Of course, one
advantages of abstract conceptions is that the compiler can
give more sensible error messages, simply because it knows more
about your intentions.
But I don't see concepts just as a tool to give error messages.
I see concepts as another level of abstraction, just as types are
a level of abstraction and not just a way to get more sensible
errors from a compiler than, say, "error: variable n1 is too small
to hold the contents of variable n2."
>=20
> There are some drawbacks to this, though. First, the name "foo" in the
> examples above is unnecessary. One would like to just write
>=20
> EqualityComparable<double, int>;
Well, with reusing of the explicit template instantiation syntax,
it's basically that (except for the extra "concept" keyword in
front of it).
>=20
> One also would like to have to write only one concept definition to
> check concepts either "by type" or "by expression":
IMHO concept definition shold _alays_ be "by type".
Concepts describe properties of types (in my original
proposal a sort of meta-type; in the current version
it's generalized to relations between types (where
"meta types" are just relations of only one type).
>=20
> EqualityComparable<T1, T2>;
> EqualityComparable(a+b, c); // want this to be equivalent to
> // EqualityComparable<typeof(a+b), typeo=
f(c)>;
>=20
> Maybe this shows that the a-concept-is-a-template view is somewhat a
> hack (or maybe that we actually need typeof()).
Well, typeof would be quite useful in templates anyway.
But that's independant from the concept idea.
BTW, I like the concept-is-a-template view (after all, even
in my first proposal concepts were checked via an implied
template function).
>=20
> Another--somewhat separate--issue are compiler warnings that might be
> produced when compiling a concept instantiation. One would need to be
> careful to write concepts such that they don't produce warnings that
> might confuse the user. One example is the unused return value of
> operator=3D=3D in the examples above, which probably should be casted t=
o
> void. But this might not be sufficient. We probably don't want warnings
> to be produced when a concept instantiation compiles successfully. But
> when it does not compile successfully, warnings and errors could be ver=
y
> helpful to find out why the concept fails (we especially want this if
> the concept definition turns out to contain bugs), instead of only
> "concept instantiation `YourConcept<T1, T2>' failed". OTOH, those
> warnings and errors could be as confusing as today's error messages
> involving templates.
Most compilers have different warning levels. For example, the
default mode could be to only report failings to conform to a
concept, while an option -Wdetailed-concepts would report details
(i.e. errors and warnings generated inside the concept).
Also, there should be a warning mode (possibly the default)
which generates a warning if the concept works only because
of a compiler-specific extension, and wouldn't work otherwise
(the specific case I have in mind is the g++ extension which
allows binding rvalues to non-const references).
>=20
> A further thought is that one might want to check concepts without
> having compilation of the program fail when a concept fails, but instea=
d
> select different implementations depending on whether a template
> argument supports some concept or not.
Well, basically that's the point about the template overloading
based on concept conformance I proposed.
Of course, if concepts are not tied to one type, then the
checking gets more complicated, too.
Again, reusing the template to declare a set of types conforming
to a concept, one could get "conformance hierarchies":
template<typename T: RandomAccessIterator<T> >
concept BidirectionalIterator<T>;
// Random access ietarators are bidirectional iterators
template<typename T: BidirectionalIterator<T> >
concept ForwardIterator<T>;
// bidirectional iterators are forward iterators
template<typename T1, typename T2:
ConvertibleTo<int, T1>, ConvertibleTo<int, T2> >
concept EqualityComparableTypes<T1, T2>;
// if both T1 and T2 are convretible to int, they are
// equality comparable (OK, this proposition _may_ be false
// for some types, due to possible ambiuities)
Concept overloading would then be based on such hierarchies:
// the algorithm works on forward iterators
template<typename Iter: ForwardIterator<Iter> >
void modify(Iter first, Iter last);
// but there's an improved algorithm for random access iterators
template<typename Iter: RandomAccessIterator<Iter> >
void modify(Iter first, Iter last);
Now if you call
modify(foo.begin(), foo.end());
the compiler would first determine the primary candidate functions
by just ignoring concepts at all. As second step, it would for
each candidate function test if the types of foo.begin() and
foo.end() conform to the required concepts of the candidate.
All candidates where the types don't conform, would be removed.
Now there are three possibilities:
1. There are no candidate functions left, either, because the
normal rules didn't find any suitable functions (the set of
primary candidate functions is empty), or because all candidate
functions failed the concept test.
2. There is exactly one candidate function left. Then it's easy:
Just call it.
3. There is more than one candidate function left (i.e. without
testing concepts, there was an abiguity, and removing functions
based on conformance tests did not completely resolve that
ambiguity). Then a partial ordering based on the confarmance
declaration is determined (this is the weak point: I'm not
sure if this is generally possible; determining an algorithm
to do this would of course a prerequisite to implementation
of that overloading feature, be it in an implementation or
in the standard). Based on this partial concept ordering,
the best candidate function is chosen. If there is not an
unambiguous best candidate function, the program is ill-formed.
So, the overloading on concept would be sort of an "afterburner"
to the normal overload resolution. It can resolve ambiguities
which could still remain (either by removing further candidates
or - if after that step the ambiguity remains - by using an
additional partial ordering), but it also disallows certain
calls (by removing the last candidate function, if the types
don't conform to the concepts).
> Unfortunately, the concept
> syntax/semantic proposed above doesn't quite fit with this.
Yes, that's the disadvantage of compile time assertions
versus true interfaces. You basically defined compile-time
assertions. I proposed new interfaces. The fact that the
definition of both looked quite similar didn't change the fact
that the underlying concept is quite different.
Since I proposed interfaces, I could just extend the
overloading rules to use those interfaces in addition
to the other interfaces (under the condition that
resolving the "hierarchy" is possible; but if not, some
intelligently set constraints might make it so, just as
templates have some constraints which arise just from
the problem of resolving them).
Somehow the fact that I am proposing interfaces seems not
to be seen (you are not the only one who replied to my
proposal with an "improvement" which only gave compile
time assertions). Maybe all people are too much fixated
on getting better error messages to see the big bicture
behind it?
Or is it just that my big picture is a halluzination? ;-)
> Maybe one
> approach is to have some sort of compile-time exceptions (shriek!) that
> are thrown when a concept fails, and that can be caught and handled by
> outer declarations, where only uncaught compile-time exceptions cause
> the compilation of the translation unit to fail.
Well, this idea is the best proof that you are thinking in
terms of compile time assertions.
And IMHO it also shows why thinking in interfaces is more
powerfull than thinking in assertions.
---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.research.att.com/~austern/csc/faq.html ]