Topic: Concepts in C++ ? (Was: Pre-proposal: user-defined error messages)


Author: Valentin.Bonnard@free.fr (Valentin Bonnard)
Date: Wed, 11 Apr 2001 21:25:59 GMT
Raw View
> 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;

and sometimes

  CopyConstructible c = CopyConstructible();

Both is one too much (for my taste).

For the overloading of templates, I propose that we keep the existing
functions overloading rules w/ MI.

--

Valentin Bonnard

---
[ 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: news/comp.std.c++@nmhq.net (Niklas Matthies)
Date: Sun, 15 Apr 2001 21:20:35 GMT
Raw View
On Sun, 15 Apr 2001 01:07:12 GMT, Christopher Eltschka <celtschk@dollywoo=
d.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.
>=20
> Outside the concept definition, the concept name names, of
> course, the concept.

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 imagine
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 this
type. For example, when the requirement "T1 can be compared for equality
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).

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:

   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; }
   };


   EqualityComparable<int, double> foo;    // concept check

   template <typename T1, typename T2>
   class MyClass
   : EqualityComparable<T1, T2>        // concept check
   {
      // or here:
      EqualityComparable<T1, T2> foo;  // concept check
      ... actual class declarations ...
   };


   void f()
   {
      ...
      int a;
      double b;
      EqualityComparable::do_check(a, b);    // concept check
      ...
   }

   concept <typename T>       // specialization
   struct EqualityComparableWithInt : EqualityComparable<T, int> { };

One could also write:

   concept <typename T1, typename T2>     // concept definition
   void EqualityComparable(const T1& t1, const T2& t2)
   { t1 =3D=3D t2; t2 =3D=3D t1; }

   void f()
   {
      ...
      int a;
      double b;
      EqualityComparable(a, b);  // concept check
      ...
   }

When the compiler fails to compile a concept instantiation, it outputs
the current concept instantiation path (for nested concept instantiations=
).
If a concept instantiation compiles successfully, no code or symbol
table entries are generated for it.

There are some drawbacks to this, though. First, the name "foo" in the
examples above is unnecessary. One would like to just write

   EqualityComparable<double, int>;

One also would like to have to write only one concept definition to
check concepts either "by type" or "by expression":

   EqualityComparable<T1, T2>;
   EqualityComparable(a+b, c); // want this to be equivalent to
                               // EqualityComparable<typeof(a+b), typeof(=
c)>;

Maybe this shows that the a-concept-is-a-template view is somewhat a
hack (or maybe that we actually need typeof()).


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 to
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 very
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.


A further thought is that one might want to check concepts without
having compilation of the program fail when a concept fails, but instead
select different implementations depending on whether a template
argument supports some concept or not. Unfortunately, the concept
syntax/semantic proposed above doesn't quite fit with this. 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.

-- Niklas Matthies

---
[ 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: Valentin.Bonnard@free.fr (Valentin Bonnard)
Date: Tue, 17 Apr 2001 22:33:38 GMT
Raw View
Niklas Matthies wrote:

>   EqualityComparable<int, double> foo;    // concept check
                                    ^^^
Yecch !

>      EqualityComparable::do_check(a, b);    // concept check

This is _not_ a valid syntax if EqualityComparable
is a class template. Argument deduction don't work
that way; if would have been valid if do_check was
a template member of a non-template class.

>   class MyClass
>    : EqualityComparable<T1, T2>        // concept check

Do you really want private inheritance (if access
means anything) ?

> Maybe this shows that the a-concept-is-a-template view is somewhat a
> hack

I believe so.

> or maybe that we actually need typeof()

We probably do.

> OTOH, those
> warnings and errors could be as confusing as today's error messages
> involving templates.

Indeed, I don't see many advantages of your proposal to the current
practice of concept checks. For example, the ``no code generated''
can already be expected if the checking functions are inline and
never called.

--
Valentin Bonnard

---
[ 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: news/comp.std.c++@nmhq.net (Niklas Matthies)
Date: Tue, 17 Apr 2001 23:19:22 GMT
Raw View
On Tue, 17 Apr 2001 22:33:38 GMT, Valentin Bonnard <Valentin.Bonnard@free=
.fr> wrote:
> Niklas Matthies wrote:
[=B7=B7=B7]
> >      EqualityComparable::do_check(a, b);    // concept check
>=20
> This is _not_ a valid syntax if EqualityComparable=20
> is a class template. Argument deduction don't work=20
> that way; if would have been valid if do_check was=20
> a template member of a non-template class.

Right, my bad.

> >   class MyClass
> >    : EqualityComparable<T1, T2>        // concept check
>=20
> Do you really want private inheritance (if access=20
> means anything) ?

Access should be irrelevant here, since the derivation above serves only
to check whether EqualityComparable<T1, T2> compiles within that
definition; but if it compiles, it is deleted (i.e. you can't access
MyClass<T1, T2>::EqualityComparable even if it was declared public).
The idea was that concept classes and functions are compiled the same
way template classes and functions are compiled, but are not taken into
account for anything other than this process of their compilation.
To put it another way, they only exist in the syntax subtree of the
declaration or expression that instantiates them, but are invisible from
the rest of the syntax tree. Conceptually, this is exactly what we want;
we want to check whether something _would_ compile, but we don't want
this something to actually be compiled into our program.

> > OTOH, those warnings and errors could be as confusing as today's
> > error messages involving templates.
>=20
> Indeed, I don't see many advantages of your proposal to the current=20
> practice of concept checks.

My primary proposal was the notion that a concept should be
parameterized the same way as template classes or functions are, i.e.
over a number of types and/or values, and not just one type as formerly
proposed.

I then showed one way one might possibly implement this, and some issues
that follow from this implementation, plus some issues that become
appearant through this implementation but are not necessarily specific
to that implementation. I certainly agree that this implementation
is far from being satisfying or even working, but it wasn't the actual
aim of my post to propone it; it was just to give some idea about the
above-mentioned notion.

> For example, the ``no code generated'' can already be expected if the
> checking functions are inline and never called.

Right. What we want in addition, though, are different diagnostics
(which currently looks like an inherently non-trivial issue to me), and
as I also pointed out, a compile-time check of compileability of a
certain construct without having compilation to fail altogether if that
construct doesn't compile.

-- Niklas Matthies

---
[ 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: news/comp.std.c++@nmhq.net (Niklas Matthies)
Date: Wed, 18 Apr 2001 01:22:41 GMT
Raw View
On Tue, 17 Apr 2001 23:10:06 GMT, Christopher Eltschka <celtschk@dollywoo=
d.itp.tuwien.ac.at> wrote:
[=B7=B7=B7]
> 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? ;-)

Well, maybe it's that what people mostly associate with interfaces are a
set of function signatures (and possibly typenames), which is more a=20
syntax thingy; but what is essential about your proposal goes more in
the direction of semantics (although, in the end, still pretty much
solely expressed in terms of syntax). Anyway, rereading your previous
posts, I now see what you were getting at.


[=B7=B7=B7]
> 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:
>=20
> 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.
>=20
> 2. There is exactly one candidate function left. Then it's easy:
>    Just call it.
>=20
> 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.
>=20
> 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).

Hmm, hmm, hmm. The one thing I have in mind is the following: Say I'd
like to define some algorithm A<T1, ..., Tn>. I know an efficient
implementation of that algorithm which requires T1, ..., Tn to conform
to concept K. I also know a somewhat less efficient implementation of
that algorithm with has slightly other requirements on T1, ...., Tn,
namely the concept K'. Now I'd like to define A<T1, ... Tn> in such a
way that if someone comes along and instantiates A with concrete T1,
..., Tn, then if those T1, ..., Tn conform to K, the more efficient
implementation of A is selected, and otherwise, if those T1, ..., Tn
conform to K', as a fallback, then the less efficient implementation is
selected; and otherwise compilation fails.

At the moment I don't see how to make this work with your template
overloading approach. One issue is, I think, that it is not sufficient
to have some partial ordering on the concepts, but that you have to
add the set of argument types to that ordering, i.e. you have to
determine the position of the set of argument types within that
ordering. In that ordering, a set of argument types would be "less than"
a concept if it conforms to that concept. One way to make this work
might be to have sets of types implicitly define a concept (much like
classes implicitly define an interface-in-the-classical-sense), so that
it can be treated as concept itself and be compared to other concepts.
Of course, the really hard part is the comparison function on the
concepts. Even if we find one that is semantically sound, I could
imagine that it might be too computationally expensive to be suitable
for this use.

Of course, one could make the ordering always explicit by declaration,
much like making a type hierarchy explicit by class derivation, but I
wouldn't want this to be required. A set of types should not have to be
explicitly declared as being conforming to a concept to be used in a
context requiring such conformance.

One other issue is that this still wouldn't make my example work,
because there is an ambiguity (because K and K' are not necessarily a
subset of one another) which only the implementor can resolve because he
knows one candidate is more efficient than the other, and the compiler
is not even supposed to try to guess. The only way to make this work
without something like a compile-time if-then-else would be to allow a
concept hierarchy (partial ordering) to be declared where an implication
of conformance (i.e. if K' is "less than" K and T1...n conformes to K',
then it is guaranteed that T1...n conformes to K as well) is not
enforced or checked by the compiler. The compiler would only use this
hierarchy to disambiguate template overloading (maybe this is something
you already alluded to, I'm not sure). But I think I wouldn't really
feel quite confortable with this, and it would require concept
dependencies to always be explicitly declared.

-- Niklas Matthies

---
[ 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                ]