Topic: Using "using" to catch improper template usage


Author: abrahams@motu.com
Date: 1998/09/21
Raw View
On Thu, 17 Sep 1998 15:53:17 GMT, in comp.std.c++
zivca@netvision.net.il (Ziv Caspi) wrote:

>The most difficult mechanism in C++ to use (for me)
>is writing templates. In many cases, I find myself instantiating
>a template that I wrote, only to discover (via some incomprehensible
>error message) that the template arguments do not have the
>proper form.
>
>For example, if "template<class T> class A {...};" relies on T
>having (say) a member "T::Required()", and I try to instantiate A<>
>with a class that has no such member, I can get some wierd error
>(many times not from A<> directly, but some other templates it uses).
>
>I am considering "compile-time assertions"-like mechanisms in C++
>to help catch these errors as early as possible. The best possibility
>I have so far is to use "using" and "typename" in the front-end
>template so as to get compiler errors if the instantiated arguments
>do not have the proper "form" the template requires. For example,
>
>  template< class T >
>  class A
>    {
>                                // T should have an accessible:
>    using typename T::Required; //   type        called Required
>    using T::Method();          //   method      called Method( void )
>    using T::Data;              //   member data called Data
>    //...
>    };

I must say, that's an ingenious solution to a couple of problems that are
long-standing. Namely, incomprehensible error reporting, and the lack of a
simple declarative method of showing a template's requirements on its
parameters.

>And the questions are:
>1. Is this guaranteed to work? (There are some elements that cannot
>   be checked in this manner; For example, that T has a private type.
>   However, A cannot then use these elements, so this is not a problem).

Unfortunately, no. The standard says:

4 A using-declaration used as a member-declaration shall refer to a mem-
  ber  of a base class of the class being defined, shall refer to a mem-
  ber of an anonymous union that is a member of  a  base  class  of  the
  class  being  defined, or shall refer to an enumerator for an enumera-
  tion type that is a member of a base class of the class being defined.
  [Example:
  class C {
          int g();
  };

  class D2 : public B {
          using B::f;             // OK: B is a base of D2
          using B::e;             // OK: e is an enumerator of base B
          using B::x;             // OK: x is a union member of base B
          using C::g;             // error: C isn't a base of D2
  };

>2. Can you think of a better method?

Well, you could amend your method as follows:

  template< class T >
  class A
  {
    struct TRequirements : T
    {
                                    // T should have an accessible:
        using typename T::Required; //   type        called Required
        using T::Method();          //   method      called Method( void )
        using T::Data;              //   member data called Data
    }
    //...
  };

>3. Are there any obstacles I overlooked?

Yes, well, of course this method requires that T be a struct or class which
can be subclassed (yes, it is possible to prevent subclassing). So built-in
types won't work as type parameters. Also, the standard goes on to say:

[Note: since constructors and destructors do not have
  names, a using-declaration cannot refer to a constructor or a destruc-
  tor  for  a base class...]

And also, you might want to require that a type be able to be compared with
operator==(), for example. But operator==() can be either a member or a free
function, and if it is a free function, there's no appropriate
using-declaration you can write. And I'm not even sure whether the nested
class TRequirements is required to be instantiated if it's not explicitly
used, which might defeat this whole approach... But don't give up yet; maybe
you can find a way around these problems. It looks promisi

-----== Posted via Deja News, The Leader in Internet Discussion ==-----
http://www.dejanews.com/rg_mkgrp.xp   Create Your Own Free Member Forum
---
[ 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://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: Alexandre Oliva <oliva@dcc.unicamp.br>
Date: 1998/09/21
Raw View
abrahams  <abrahams@motu.com> writes:

> On Thu, 17 Sep 1998 15:53:17 GMT, in comp.std.c++
> zivca@netvision.net.il (Ziv Caspi) wrote:

>> I am considering "compile-time assertions"-like mechanisms in C++
>> to help catch these errors as early as possible.

>> template< class T >
>> class A
>> {
>> // T should have an accessible:
>> using typename T::Required; //   type        called Required

`using' is just an alias to a name, be it a typename or not.  IMO,
this declaration should be rejected.

>> using T::Method();          //   method      called Method( void )

And so should this one.  Remember, using has to do with names; what
the name means is not important.

> I must say, that's an ingenious solution to a couple of problems that are
> long-standing. Namely, incomprehensible error reporting, and the lack of a
> simple declarative method of showing a template's requirements on its
> parameters.

>   template< class T >
>   class A
>   {
>     struct TRequirements : T

This won't necessarily fail, unless A<T>::TRequirements is explicitly
referenced elsewhere.  Here's an alternate solution I've just
designed:

namespace Requirements {
  template <typename T> struct A {
#ifdef EGCS_BUG
    struct test {
      test()
#else
    typedef A test;
    A()
#endif
 { if (0) for_T(); }
    private:
      void for_T() { // never executed
 typedef typename T::type type; // T::type must be accessible
 void (T::*m)() = &T::method; // T::method must have this signature
 int T::*d = &T::data;        // T::data must have type int
 void (*sm)() = &T::stmethod; // static method
 int *sd = &T::stdata;        // static data member
 T* t = new T(/*args*/);      // constructors
 delete t;                    // destructor
      }
#ifdef EGCS_BUG
    };
#endif
    static test check;
  };
  template <typename T> A<T>::test A<T>::check;
}
template <typename T,
          Requirements::A<T>::test*/*unnamed*/ = &Requirements::A<T>::check >
class A {};
class t {
#ifdef PUBLIC
public:
#endif
  ~t() {};
#ifdef MEMBERS
  typedef int type;
  type data;
  static type stdata;
  void method() {};
  static void stmethod() {};
#else
  t(int);
#endif
};
#ifdef MEMBERS
t::type t::stdata;
#endif
A<t> test;

Compiled with egcs 1.1 -DEGCS_BUG (without -DMEMBERS), this code
snippet produces:

test.cc: In method `void A<t>::test::for_T<t>()':
test.cc:10:   instantiated from `A<t>::test::test<t>()'
test.cc:26:   instantiated from here
test.cc:13: no type named `type' in `class t'
test.cc:13: warning: ANSI C++ forbids typedef which does not specify a type
test.cc:14: `method' is not a member of type `t'
test.cc:15: `data' is not a member of type `t'
test.cc:16: `stmethod' is not a member of type `t'
test.cc:17: `stdata' is not a member of type `t'
test.cc:18: no matching function for call to `t::t ()'
test.cc:45: candidates are: t::t(const t &)
test.cc:43:                 t::t(int)
test.cc:35: `t::~t()' is private
test.cc:19: within this context

With -DEGCS_BUGS -DPUBLIC -DMEMBERS, it only complains that main() is
undefined.

Of course, egcs should have written Requirements::A<t>:: in the error
messages.  Furthermore, the helper class Requirements::A<t>::test is
only needed because otherwise egcs would complain that
Requirements::A<t> is not a complete type.

Unfortunately, this requires the declaration of the template class A
to be modified, but the error messages are quite clear.  Note that,
with this separation between the template class and its requirements,
specializing a template class does not necessarily specializes its
requirements, but it is possible to specialize them too.

The main drawback of this approach is that, if the template is given
privileged (friend) access to a class, so must the Requirements
tester.

> And also, you might want to require that a type be able to be compared with
> operator==(), for example.

`*(T*)0 == *(T*)0' within for_T() should do it.  Note that it does not
invoke undefined behavior, because for_T() is never executed.

--
Alexandre Oliva
mailto:oliva@dcc.unicamp.br mailto:aoliva@acm.org
http://www.dcc.unicamp.br/~oliva
Universidade Estadual de Campinas, SP, Brasil
---
[ 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://reality.sgi.com/austern_mti/std-c++/faq.html              ]