
"Chad Parry" <spam@chad.parry.org> writes:
I would like to know if many people in the community think the following library would be useful.
I would like to define concepts using this syntax:
template<typename left_type, typename right_type = left_type> struct less_than_comparable { static var<left_type const> left; static var<right_type const> right; static var<bool> b; typedef CONCEPTS_USE_PATTERN(b = (left < right)) constraints; };
Chad, this looks like really interesting work. I should point out that I have recently changed the Boost concept checking library to support a syntax much closer to that of conceptc++ (http://www.generic-programming.org/languages/conceptcpp/). In particular: 1. you can access associated types through concepts, e.g.: template <class I> boost::ForwardIterator<I>::difference_type distance(I start, I finish); 2. BOOST_CONCEPT_ASSERT((SomeConcept< ... >)) works in all contexts and produces readable error messages. 3. You can refine concepts via inheritance, with no need to explicitly assert the base concepts: template <class TT> struct InputIterator : Assignable<TT> // HERE , EqualityComparable<TT> // HERE { // associated types typedef typename boost::detail::iterator_traits<TT>::value_type value_type; typedef typename boost::detail::iterator_traits<TT>::difference_type difference_type; typedef typename boost::detail::iterator_traits<TT>::reference reference; typedef typename boost::detail::iterator_traits<TT>::pointer pointer; typedef typename boost::detail::iterator_traits<TT>::iterator_category iterator_category; BOOST_CONCEPT_ASSERT((SignedInteger<difference_type>)); BOOST_CONCEPT_ASSERT((Convertible<iterator_category, std::input_iterator_tag>)); // valid expressions ~InputIterator() { TT j(i); (void)*i; // require dereference operator ++j; // require preincrement operator i++; // require postincrement operator } private: TT i; }; 4. You can use "where" clauses on generic functions: template<typename RanIter> BOOST_CONCEPT_WHERE( ((Mutable_RandomAccessIterator<RanIter>)) ((LessThanComparable<typename Mutable_RandomAccessIterator<RanIter>::value_type>)) , (void)) sort(RanIter,RanIter) { ... } I think there are probably other features I'm forgetting; it's been a few months ;-)
In the code above the less_than_comparable<T, U> concept is defined. The var<> type is meant to resemble the Var<> in Stroustrup's concept proposals.
I don't recommend trying to emulate those proposals unless you mean the very recent ones written with the authors of conceptc++. The concept support we get is probably going to look a lot more like conceptc++ than like the early Stroustrup/Dos Reis proposals.
The CONCEPTS_USE_PATTERN(EXP) macro expands to BOOST_TYPEOF(EXP). The b, left and right variables have lambda-like capabilities where you can apply any operation on them and they return a complex type that remembers what operations have been applied.
Functions that want to enforce concepts would use this syntax:
template<typename operand_type> bool my_less_than(operand_type left, operand_type right) { // Enforce the model. typedef less_than_comparable<operand_type> model; BOOST_STATIC_ASSERT(is_match<model>::value);
That won't produce a readable error message.
constrained_proxy<1, model> constrained_left(constrain<1, model>(left)); constrained_proxy<2, model> constrained_right(constrain<2, model>(right)); // Use the constrained proxies just like regular types. return (constrained_left < constrained_right); }
Obviously this syntax is more cumbersome than the current Boost Concept Check Library's. But it offers more power too:
1) The author doesn't have to hand-craft an archetype class! The model knows what operations should be allowed and the constrained_proxy<> class enforces that only those operations are taken.
Ah, so. The point here is that if you can instantiate my_less_than with something (anything) without causing an error, my_less_than can only be using operations allowed by the model, because the function only really operates on the constrained_proxy. Very clever.
2) It is possible to check whether a model is valid without causing a fatal error.
I don't believe that can be done in general. There are many usage patterns (or pseudosignatures as in conceptc++) that *can't* be checked in C++98 without causing an error. For example, we have no non-fatal test for default constructibility. How would you handle that in your system [I see the answer below]?
That's what the is_match<model> metapredicate is for. This could be used to dispatch different optimized code depending on which concepts the type is known to model. Now, there are some things that can't be detected by library code (like the presence of a default constructor)
Aha! So in what sense does your system make that non-fatal checking possible where it wasn't possible before? Oh, it encodes the usage pattern in a type, which is then converted into a metafunction. Very very clever. Unless I'm missing something, you can't represent lots of important usage patterns other than default construction, e.g.: std::swap(x, y) ??
so I would have to rely on authors to provide hints about those. I'm planning on just assuming that any requirements I can't detect are fulfilled, and then the compiler will complain if that's wrong.
That defeats one of the main purposes of concept checking: early detection of errors before a large instantiation backtrace is generated.
3) The concepts can be built up from primitives using and_ and or_ and not_ operations, just like in Stroustrup's proposals. For example, the concept above could have included this statement instead: "typedef and_<CONCEPTS_USE_PATTERN(b = left < right), CONCEPTS_USE_PATTERN(b = right < left), copy_constructible<left> > constraints;"
I think I'd better leave it to Doug Gregor to explain why "or" constraints are problematic.
4) This use pattern syntax more closely resembles the C++0x use pattern proposals than any other existing library.
Unless you're looking at the wrong proposals ;-) Usage patterns don't really work for compiler support of concept checking, because they hide intermediate types that need to be explicit.
5) The constrained_proxy class prevents certain errors where a pathological type behaves unexpectedly. I'm talking about functions like "evil_bool_proxy operator<(mytype, mytype)" where evil_bool_proxy defines its own && and || operators. In the expression, "(a < b) && (c < d)" the constrained_proxy enforces that the && operation gets applied to bool values, not to evil_bool_proxy values.
6) This mechanism allows you to express that a model has important semantic implications also. The library assumes that a model is valid if it is a structural match. But you can override that by saying, "template<typename value_type> declare_match<forward_iterator<istream_iterator<value_type> > > : false_ { };"
That's fine if you're doing it just for checking, but it's not fine for dispatch. Consider what happens to vector(i1, i2) when i1 and i2 are input iterators that structurally mimic forward iterators.
7) Traits classes could potentially be added to this library so that authors could express mappings such as that the constrained_proxy should call a free function "push_back(my_seq, elem)" instead of the normal "my_seq.push_back (elem)" for a given sequence type.
So far I have gotten var<> and declare_match<> and is_match<> to compile. I've also finished and_<>, or_<> and not_<>. I'm working on constrained_proxy<>.
Well, it looks really interesting (really!), but I think you should review the ConceptC++ publications, in particular N1849, before investing more time in it. Also, IMO, you should try to model the standard concepts and see how it works out in practice. Lots of food for thought in your work, for sure. -- Dave Abrahams Boost Consulting www.boost-consulting.com