Friday, 24 October 2008

C++: Changing the meaning of a name

We've just bumped the version of gcc we use to build our codebase from 4.2.2 to the shiny, cutting edge 4.3.2. This involved the usual number of small tweaks to our source, to make it compatible with the latest gcc parser.

One change surprised me, though. The following pattern is no longer accepted by gcc:
template <typename T>
class Producer
{
public:
/// ...
};

template <typename T>
class Consumer
{
public:
typedef Producer<T> Producer;
};

void ThisWontCompile()
{
Producer<int> producer;
Consumer<int> consumer;
}
gcc now generates the following error:
test.cpp:13: error: declaration of 'typedef class Producer Consumer::Producer'
test.cpp:4: error: changes meaning of 'Producer' from 'class Producer'
We use this pattern quite a lot in our code, and it seems reasonably tasteful. Certainly, it seems morally equivalent to the following pattern:
class Foo
{
public:
/// ...
};

class Bar
{
public:
void Foo() {}
};

void ThisCompiles()
{
Bar bar;
}
...in the sense that they both change the meaning of a name within the scope of the second class.

Now I'm no C++ language lawyer, but I've yet to find a compelling part of the standard that shows this to be bad C++ code.

Update

Commenter jd pointed out the solution to this conundrum. The Producer example above will compile if the typedef is written slightly differently, thus:
template <typename T>
class Producer
{
public:
/// ...
};

template <typename T>
class Consumer
{
public:
typedef ::Producer<T> Producer;
};

void ThisDoesCompile_Hurrah()
{
Producer<int> producer;
Consumer<int> consumer;
}
This shows that the gcc change was not as drastic as I first feared, and the fix is still very valid (and very readable) C++ code.

The problem was gcc looking up the name Producer in the scope of the Consumer class, and finding the Producer typedef name that was in the process of being defined. A subtle problem with a non entirely useful error message (but we're used to those, aren't we?).

Although the other compilers I have tried (MSVC and Comeau) accept the original code, I'm not sure if it is correct according to the letter of the C++ standard or not.

10 comments:

Anonymous said...

Those two are not equivalent...

The problem is that you have a template, Producer<T>, and then you want to have a type Producer. The "morally equivalent" code doesn't do that, does it?

Did I miss something?

Anonymous said...

Why would a typedef and a function be morally equivalent? And you wrote a programming book?

Just change the typedef name.

Pete Goodliffe said...

Clearly a typedef and a function are not technically equivalent in any way. However, I say they are morally equivalent because inside the Consumer/Bar class, you have changed the meaning of a name in both cases. gcc allows one kind of name change, but not the other.

If you want something closer technically, as well as morally, you could use this example:


class Foo {};
class Bar
{
public:
    class Foo {};
    void WhichFooIsUsedInHere()
    {
        Foo foo;
        // Bar::Foo of course
    }
}


That compiles fine under gcc 4.3.2. It's not ambiguous.

The more I look at this, the more I think that the Producer/Consumer example is valid C++ and that the latest gcc is doing something weird. But, as I say, I'm not a C++ lanugage lawyer, so I won't bet my life on it.

Many other compilers accept the questionable code, BTW.

And, of course, I have changed the template names throughout our codebase. But when maintaining a large codebase, having to make such modificatons to a prevelent pattern is not ideal, if the code actually is valid C++.

Pete Goodliffe said...

It's worth pointing out why simply changing the typedef name is not my preferred solution to get this code to compile. It's down to the motivation for using the typedef in the first place...

It's hard to show the importance of the typedef in such a small, sanitised code snippet. But I'll try to explain.

Whenever any of the implementation of Consumer<T> refers to an instance of the Producer class (and it does so a lot), it's always talking about a Producer<T>. So the typedef is setting up a more convenient name for Producer<T>.

Imagine this with various different (more descriptive) names for "Producer" and for "T" and you'd begin to see why the typedef is a useful construct in this case.

This is a pattern used across many, many pairs (or triplets, or...) of classes.

The typedefs help make much more readable code.

So in gcc 4.3.x, having to name the typedef something other than Producer isn't making the code easier to write and to read any more. It's made it harder, since there's another name to understand now. It would probably be easier to stick to using Producer<T> throughout the code.

Sigh.

Anonymous said...

ISO 14882-2003, 3.3.6, basic.scope.class

2) A name N used in a class S shall refer to the same declaration in its context and when re-evaluated in the completed scope of S.

Pete Goodliffe said...

Perhaps I'm being stupid, but I don't see how 3.3.6, basic.scope.class applies to this problem. I'm not sure that is does.

Could you explain how 3.3.6explains the behaviour I described, and why template constructs are not allowed by non-template constructs are?

Jon said...

One work around appears to be to fully qualify the namespace of the type used in the typedef. So to use your earlier example and add in an explicit global namespace reference:

template <typename T>
class Producer
{
public:
/// ...
};

template <typename T>
class Consumer
{
public:
typedef ::Producer<T> Producer;
};

void ThisWillCompile()
{
Producer<int> producer;
Consumer<int> consumer;
}

Pete Goodliffe said...

jd - you are spot on. Many thanks.

So the gcc change is a lot less dramatic than it at first appeared. Indeed, it does seem sensible to qualify which "Producer" class you are typedefing inside Consumer.

susi said...

Hai Pete,
Nice information you have got here. I am not a big user of templates.
But how did you come across with this problem?
Actually i got the same problem while compiling stl with various versions of gcc. So thanks for the info. I found these problem in stl_tree.h file. How did they managed to avoid this problem in new stl_tree.h with new versions of stl. You have any idea?

Anonymous said...

Hai pete,
I think -fpermissive option for gcc is also helpful to change the error to warnings.Please check.
Best Regards,
Susmith M R