0

Hi there,

While making a CRTP-based generic wrapper to call arbitrary library functions, I've encountered a problem which I have trouble understanding. Here is a very simplified code to illustrate the problem:

#include <iostream>

template< typename PValue, typename PDerived >
class TBase
{
 private:
  typedef TBase TSelf_;
  typedef PDerived TDerived_;

 protected:
  typedef PValue TValue_;

 protected:
  TBase( void )
  {
   std::cout << " TBase::TBase() " << std::endl;
  }

 public:
  void Foo( void )
  {
   std::cout << " TBase::Foo() " << std::endl;
  }

  template< typename PType >
  static void Call( PType /*pSomething*/, void(TDerived_::*pFunction)( void ) = &TSelf_::Foo, TDerived_ pDerived = TDerived_() )
  {
   ( pDerived.*pFunction )();
   std::cout << " static TBase::Call(). " << std::endl;
  }
};

template< typename PValue >
class TDerived : public TBase< PValue, TDerived< PValue > >
{
  friend class TBase< PValue, TDerived< PValue > > ;
 private:
  typedef TBase< PValue, TDerived > TBase_;
  typedef TDerived TSelf_;
 public:
  TDerived( void ) :
   TBase_()
  {
   std::cout << " TDerived::TDerived() " << std::endl;
  }
  void Foo( void )
  {
   std::cout << " TDerived::Foo() " << std::endl;
  }
  void Bar( void )
  {
   std::cout << " TDerived::Bar() " << std::endl;
  }
};

int main( void )
{
 TDerived< int >::Call( 1 );
 TDerived< int >::Call( 1, &TDerived< int >::Foo );
 TDerived< int >::Call( 1, &TDerived< int >::Bar, TDerived< int > () );
 return ( 0 );
}

Everything compiles and works as intended. However, if I try to use pointer to TDerived::Foo() as a default argument for the second parameter in TBase::Call(...):

static void Call( PType /*pSomething*/, void(TDerived_::*pFunction)( void ) = &TDerived_::Foo, TDerived_ pDerived = TDerived_() )

compilers gives a syntax error... I have a feeling it is related to how compiler parses code and that it cannot figure out pointer to a function of yet to be defined (or instantiated) class. However, it has no problem calling TDerived constructor as a default argument for the third parameter of TBase::Call(...). Can someone give me a definite answer about what's going on? Why derived class MFP is not accepted, and object of derived class is accepted as default arguments?

Thanks.

EDIT: compiler's error (MSVS2010 command line compiler):

FMain.cpp(224) : error C2061: syntax error : identifier 'TDerived_'; FMain.cpp(233) : see reference to class template instantiation 'TBase<PValue,PDerived> with [PValue=int,PDerived=TDerived<int>]' being compiled; FMain.cpp(323) : see reference to class template instantiation 'TDerived<PValue> with [PValue=int]' being compiled

It's a syntax error - it does not recognize TDerived_ as type in default argument for MFP. There are other errors following this one, they are all syntax errors, since function definition is ill-formed now. That is how I understand it.

EDIT: Basically, I don't understand why can I use an object of TDerived_ as a default argument, but can not use a pointer to a member function as a default argument.

EDIT: Ok, this is driving me crazy now. First of all, I changed to typedef TBase< PValue, TDerived > TBase_; as it was pointed out (thank you, guys!). Indeed, it only compiled under MSVC++, since this compiler does not do two-part parsing; i.e., on codepad.org (which uses g++ 4.1.2) it didn't compile. Second, after that, I tried to use static void Call( PType /*pSomething*/, void(TDerived_::*pFunction)( void ) = &TDerived_::Foo, TDerived_ pDerived = TDerived_() ) on codepad.org and... it compiled and run correctly! So I'm REALLY confused now: people explained to me why it's not correct (and I couldn't understand "why" (see my previous EDIT)) and now it turns out g++ compiles it correctly... Does it mean it just MSVC++ problem and not the code? Or code does have a problem from the Standard point of view (and I cannot see it) and g++ accept it "by mistake" (unlikely, I think)?.. Help?!

lapk
  • 3,838
  • 1
  • 23
  • 28
  • What error compiler gives? Why don't you post that as well? Is it that you want us to compile this code, and see the error ourselves? – Nawaz Nov 19 '11 at 04:43
  • A "syntax error" error message is the least useful diagnostic message possible (except "there is an error somewhere"). – curiousguy Nov 19 '11 at 07:50
  • Re: your most recent comments - the code looks OK to me after the typedef change, but maybe someone with more extensive knowledge of the standard can comment. FWIW, after the change it compiles and runs for me on VS2008 and GCC 4.2.1. – clstrfsck Nov 20 '11 at 00:29

2 Answers2

2

The scoping for the TValue_ parameter to the type in the typedef for TBase_ in TDerived appears to be wrong (!)

You have:

 private:
  typedef TBase< TValue_, TDerived > TBase_;

I think you need:

 private:
  typedef TBase< typename TBase< PValue, TDerived< PValue > >::TValue_, TDerived > TBase_;

Or even just:

 private:
  typedef TBase< PValue, TDerived > TBase_;

EDIT: The C++ standard section 14.6.2 para 3 covers this case:

In the definition of a class or class template, if a base class depends on a template-parameter, the base class scope is not examined during unqualified name lookup either at the point of definition of the class template or member or during an instantiation of the class template or member

clstrfsck
  • 14,715
  • 4
  • 44
  • 59
  • `TValue_` is defined in `TBase` as protected `typedef`. `TDerived` inherits publicly from `TBase` and has access to its protected definitions. – lapk Nov 19 '11 at 06:48
  • @AzzA Actually msandiford did not say that `TValue_` was not accessible in this context, he said it was not properly "scoped". He means that `TValue_` **is not visible here**. – curiousguy Nov 19 '11 at 07:38
  • For an explanation of why `TValue_` **cannot** be visible in the derived class **in C++**, see [Using this keyword in destructor (closed)](http://stackoverflow.com/questions/7908248/using-this-keyword-in-destructor/7908530#7908530) – curiousguy Nov 19 '11 at 07:49
  • @msandiford and curiousguy: thanks for the info, guys! So, obviously, the code is faulty (probably, in many ways) from the Standard point of view, it's just that MS compiler accepts it due to not having two-phase name look-up. Well, that sucks... – lapk Nov 19 '11 at 08:42
  • @AzzA The way MS handles templates is totally different from what the standard mandates (2 phase name lookup). The rules for templates were not clear in the early days of C++ templates (but if you read D&E, you can see that BS wanted something like 2 phase name lookup from the beginning), and parsing rules have been changed, notably requiring programmers to add `typename` all over the place. But **these modern template rules already existed in 1998**, so if MS clearly made a choice to not be standard conforming here. – curiousguy Nov 19 '11 at 16:08
  • (...) It is easy to see that modern templates name lookup rules do not completely rule out the kind of interference between declarations visible "by accident" and templates they were designed to avoid: if `E` is a dependant expression, in `f(E)` `f` will be looked-up in both contexts, even if the intent of the programmer is to use a function `f` declared in the local namespace. This means that "locality" of names is broken by templates, even with the modern templates. – curiousguy Nov 19 '11 at 16:31
  • (...) Remark: `export` makes no difference in this case, as it is not its job. (Sutter used this fact as an argument against `export`, saying that people, even C++ experts, expected `export` to fix templates name lookup - a claim he could not back-up with citations.) So, it can be argued that old-style macro-like template that MS implements are not only easier to use, they also **cause less surprises than modern lookup rules: modern template parsing rules do not accomplish much, often are a pain, are not easy to explain and teach, and break template inheritance scoping.** – curiousguy Nov 19 '11 at 16:34
  • (...) The problem is that **MS does not implement C++, and that many users are not fully aware of that.** I often complain that MS makes really dumb design choices. IMO in this case, the dumb design choices were made by C++ designers. But **MS is making it harder for programmers to write standard conforming C++**, a king of things they did in quite a lot of contexts (IE...). – curiousguy Nov 19 '11 at 16:37
  • "_The C++ standard section 14.6.2 para 3 covers this case:_" when you first read this, did you had a WTF moment? – curiousguy Nov 19 '11 at 16:46
  • @curiousguy Do you mean it's hard to parse? My favourite is from the GCC manual for the "-fgraphite-identity" compiler option: "For every SCoP we generate the polyhedral representation and transform it back to gimple". – clstrfsck Nov 20 '11 at 00:32
  • @msandiford "_Do you mean it's hard to parse?_" It isn't _harder_ to parse than most of template chapter. :) And when you understand what it says, it is quite surprising (at least it was to me). At first, I believed it was an arbitrary rule. Only later came enlightenment (this rules actually follows directly from template design). "_For every SCoP we generate the polyhedral representation and transform it back to gimple_" This one is as clear as ... graphite (of course). – curiousguy Nov 20 '11 at 01:06
1

Simple: when instantiating TBase< int, TDerived< int> > template class definition, the declaration of Call<> function template is instantiated:

  template< typename PType >
  static void Call( PType , void(TDerived_::*pFunction)() = &TSelf_::Foo, TDerived_ pDerived = TDerived_() )

(with TDerived_ = TDerived< int>), which is fine as TSelf_::Foo() is declared at this point.

OTOH, the problem with

static void Call( PType , void(TDerived_::*pFunction)() = &TDerived_::Foo, TDerived_ pDerived = TDerived_() )

is that TDerived_::Foo() is not declared during TBase< int, TDerived< int> > template class definition instantiation.

BTW, you don't need to specifiy a parameter list as ( void ); () has the same effect and is less verbose.

curiousguy
  • 8,038
  • 2
  • 40
  • 58
  • yes, but what about third default argument? Why can I use 'TDerived_' temporary as third default argument? If I cannot use address of `TDerived_` member function, how can I create a `TDerived_` object in the same scope? I must be really stupid, but I still don't get it... – lapk Nov 19 '11 at 08:47
  • I suppose, I need to get a compiler that follows the Standard. It's probably pointless asking "why this compiles and that does not" when compiler fundamentally allows code that is not suppose to compile according to the Standard. Thanks for input, guys. – lapk Nov 19 '11 at 09:38
  • @AzzA "_Why can I use `TDerived_` temporary as third default argument?_" knowing that `TDerived_` is a type, the meaning of `TDerived_()` is known: construct a temporary object of type `TDerived_`. Of course, the fact that there is an exactly one constructor that can take 0 argument, and that it is accessible in this context, must be checked **later**, but that would not impact the fact that the expression is a rvalue of type `TDerived_`. OTOH to compile `TDerived_::Foo`, the compiler needs to be able able to lookup the name in order to determine the type of the expression. – curiousguy Nov 19 '11 at 21:10
  • (...) But **that's just my gut feeling.** Don't take it as definitive statement, or as a careful analysis. – curiousguy Nov 19 '11 at 21:10