Friday 5 September 2008

The incomparable joy of C++ inner classes

More on my love/hate relationship with my programming tool of choice. Oh C++. Oh joy. Literally incomparable joy. Because I discovered that you can't easily compare inner classes if the outer class is a template.

You say what?

OK, take a look at this code...

#include <cassert>

template <typename T>
struct outer
{
class inner;
};

template <typename T>
struct outer::inner
{
inner(const T &a) : a(a) {}

const T a;

//With this uncommented, we find an operator== in main [1]
//bool operator==(const inner &other)
//{ return other.a == a; }
};

// This operator== is never found
template <typename T>
bool operator==(const typename outer::inner &lhs,
const typename outer::inner &rhs)
{
return lhs.a == rhs.a;
}


int main()
{
outer::inner a(10);
outer::inner b(20);

// So this doesn't compile
assert(a == a);
assert(b == b);
assert(!(a == b));
}
I naively expected that to work OK. On gcc 4.x it generates this error:

> g++ test.cpp && ./a.out
test.cpp: In function ‘int main()’:
test.cpp:35: error: no match for ‘operator==’ in ‘a == a’
test.cpp:36: error: no match for ‘operator==’ in ‘b == b’
test.cpp:37: error: no match for ‘operator==’ in ‘a == b’
If you add in the code marked as [1], an operator== is found and all is well. However, there is more flexibility in the second operator== form, and I need to be able to write one. Game over.

This is a case of a classic template problem, where the compiler cannot deduce template type of an outer class based on the inner type - a non-deduced context where the compiler is
not required to figure out what Ts would produce a match for your
inner class. To be fair, that would be rather heroic for it to figure out.

The most workable solution I can find is to move the inner class out to a separate top-level template class. This has the downside that it pollutes the enclosing namespace with detail that needn't actually be there. But it has the advantage of generating code that works, and so I can live with that.

template <typename T>
struct outer_inner
{

T a;
};

template <typename T>
bool operator==(outer_inner const& lhs, outer_inner const& rhs)
{
return lhs.a == rhs.a
}

template <typename T>
struct outer
{

typedef outer_inner inner;
};
As a compromise, I am tempted to make a "details" namespace, just to put the morally "inner" top-level classes in, so that the top-level is (relatively) unpolluted.

Why do I care about this? I'm implementing an STL-like container (more on this another time). If outer was instead spelt std::map, and inner was spelt iterator, then you can begin to see my motivation.

6 comments:

Unknown said...

What happens if you write:

friend bool operator == (const typename outer[T]::inner & lhs, const typename outer[T]::inner & rhs)
{
return lhs.a == rhs.a;
}

inside the definition of struct outer[T]::inner instead of the alternative form - or trying to write it outside?

Brackets changed to protect the useless.

Pete Goodliffe said...

Hi Rik,

I think I know what you mean there. Sadly, it won't work. Friendship doesn't make any impact on the whether that template function would be considered for the resolution of operator-- on the nested class.

The only solution is to hoist the inner class outside :-(

Unknown said...

Seems to work... or am I missing something?

#include <iostream>
#include <cassert>

template <typename T>
struct outer
{
  struct inner;
};

template <typename T>
struct outer<T>::inner
{
  inner(const T & a) : a(a) {}
  const T a;

  friend bool operator == (const typename outer<T>::inner & lhs, const typename outer<T>::inner & rhs)
  {
    std::cerr << "Using outer<T>::inner::operator ==" << std::endl;
    return lhs.a == rhs.a;
  }
};

int main()
{
  outer<int>::inner a(10);
  outer<int>::inner b(20);

  assert(a == a);
  assert(b == b);
  assert(!(a == b));

  outer<std::string>::inner sa("hello");
  outer<std::string>::inner sb("world");

  assert(sa == sa);
  assert(sb == sb);
  assert(!(sa == sb));
}

Pete Goodliffe said...

Sorry, I presumed you'd got my original aim: to move operator== out of both the inner and the outer classes, to the top-level namespace.

So then I was imaging your version would have to read something like:


template <typename T>
struct outer
{
struct inner;
};

template <typename T>
bool operator==(const typename outer<T>::inner & lhs,
const typename outer<T>::inner & rhs);

template <typename T>
struct outer<T>::inner
{
inner(const T & a) : a(a) {}
const T a;

template <typename T2>
friend bool operator==(const typename outer<T2>::inner & lhs,
const typename outer<T2>::inner & rhs);
};

template <typename T>
bool operator==(const typename outer<T>::inner & lhs,
const typename outer<T>::inner & rhs)
{
std::cerr << "Using outer<T>::inner::operator ==" << std::endl;
return lhs.a == rhs.a;
}


Which still fails to compile. (Sorry if the formatting above gets obliterated in the post)

However, I'm very intrigued by the idea of the "friend" operator== inside the class. I'd not though of that version. The reason to take the operator== outside the class (to allow types convertible to inner to participate in equality on either side of the ==) might be redundant with that version.

I'll have a play around. Thanks.

Unknown said...

It looks like gcc isn't smart enough to know what you mean when you implement the operator outside the class definition - even if you forward declare it.

It's been 5 years since I wrote C++, though, so I may be barking.

Pete Goodliffe said...

You're not barking, but it's not gcc's fault. In the context of calling operator== in main, that template operator== is not involved in name resolution as the template types are not deducable.

See
http://www.codeguru.com/forum/archive/index.php/t-395268.html