Link to home
Start Free TrialLog in
Avatar of LuCkY
LuCkY

asked on

Small yet annoying (template) overloading problem

Hi,

I'm constantly getting this compiler error and there are workarounds, but I prefer readability above those workarounds :-)

I will give you a simplified version of the code:

template <typename T>
class CBuffer
{
public:
    operator T * () const throw()
    {
        return m_buffer;
    }

    T& operator [] (UINT uPos) const throw()
    {
        return m_buffer[uPos];
    }

private:
    T * m_buffer;
};

class CString : protected CBuffer<TCHAR>
{
public:
    using CBuffer<TCHAR>::operator [];

    void Empty() throw()
    {
        (*this)[0] = '\0';
    }
};

This code gives me this error:

c:\Weltevreden Framework\Source\Utility.hpp(477) : error C2666: 'Weltevreden::Utility::CBuffer<T>::operator`[]'' : 2 overloads have similar conversions
with
[
    T=TCHAR
]
c:\Weltevreden Framework\Source\Utility.hpp(139): could be 'TCHAR &Weltevreden::Utility::CBuffer<T>::operator [](UINT) throw()'
with
[
    T=TCHAR
]
or       'built-in C++ operator[(TCHAR , int)'
while trying to match the argument list '(Weltevreden::Utility::CString, int)'

Can easily be solved by either commenting out the T * operator from CBuffer or by using an "elementAt" function instead of the "operator []". In this case it may even be better readable, but I still prefer to have the "operator []" working and it gives the same error if I use it elsewere (also want to keep the T * operator by the way :-)). The strange thing is that I don't use the type casting operator in the CString class (ie I don't do this: "using CBuffer<TCHAR>::operator T * ();"). So basically the compiler should be ignoring that operator all together as I use the CBuffer as a protected subclass shouldn't it?

Well either way, if you have an idea on how to solve this (or some suggestive imput) please share :-)
Avatar of Dexstar
Dexstar

LuCkY:

> Well either way, if you have an idea on how to solve this (or some suggestive
> imput) please share :-)

You don't really need both of those, because they do almost exactly the same thing.  If you want to keep them both, then you're going to have to clarify to the compiler which one you mean whenever you use one or the other.  I would omit the operator T*() ...  The [] operator allows you to check the index being selected and handle an error if it exceeds the allocated size.  And if you really want a T* value, you can always do this:  &(str[0]) .

Hope That Helps,
Dex*
Avatar of LuCkY

ASKER

I do need both of them as there is a big difference:

For instance I have this function:

void BogusFunction(LPCTSTR) { }

I can use this code:

CString strString;
BogusFunction(strString);

Which can only be achieved by using a T * operator (I think :-), atleast not with a [] operator).

As I said, readability matters for me, I know I can do &(strString[0]), but that's not what I want. I'd rather use a getPointer() function or something like that then.
Okay, well, if you want to use the T* operator for const values (like LPCTSTR), then the compiler might not complain.  Try changing it to this:

    operator const T* () const throw()
    {
        return m_buffer;
    }

Dex*
Avatar of LuCkY

ASKER

Hmmm, nope, that doesn't do the trick. It wouldn't be a totally satisfying solution either as I don't want the CBuffer to return a const value. The CString however will get a const operator (as soon as I get this working that is :-))
Well, sometimes you'll want a const pointer, and sometimes you won't, so you'll probably end up having both versions in your class when you're all done.  And, if you only have one, it should be the const...

But, anyway, I'm not sure of how to make both you and the compiler happy.  I think you may want to have protected "getPointer" function like you said, and use that in your Empty() function.

Dex*
>  So basically the compiler should be ignoring that operator all together as I use the CBuffer as a protected subclass shouldn't it?

No, protected inheritance means that clients of CString can't see CBuffer's members, but CString can still see all of CBuffer's public and protected members.

Why don't you just get rid of the operator [] function?  You can still subscript the pointer returned by operator T* and get the same effect.

--efn
Avatar of LuCkY

ASKER

> Why don't you just get rid of the operator [] function?  You can still subscript the pointer returned by operator T* and get the same effect.

This would indeed solve the problem. However I cannot put error checking in the code that way. It's something I could live with though. BUT, next problem arises in this case, which is actually just the same problem (or atleast one of a kind):

I have this code:

template <typename T>
class CBuffer
{
public:
    operator T * () const throw()
    {
        return m_buffer;
    }

private:
    T * m_buffer;
};

class CString : protected CBuffer<TCHAR>
{
public:
    operator LPCTSTR() const throw()
    {
        return (LPTSTR)(*this);
    }

    void Empty() throw()
    {
        (*this)[0] = '\0';
    }
};

And get:

c:\\Weltevreden Framework\Source\Utility.hpp(482) : error C2593: 'operator [' is ambiguous
could be 'built-in C++ operator[(TCHAR , int)'
or       'built-in C++ operator[(LPCTSTR, int)'
while trying to match the argument list '(Weltevreden::Utility::CString, int)'

Also tried:

template <typename T>
class CBuffer
{
public:
    operator T * () const throw()
    {
        return m_buffer;
    }

    operator const T * () const throw()
    {
        return m_buffer;
    }

private:
    T * m_buffer;
};

class CString : protected CBuffer<TCHAR>
{
public:
    using CBuffer<TCHAR>::operator LPCTSTR;

    void Empty() throw()
    {
        (*this)[0] = '\0';
    }
};

But this gives the same results. Why do I want CBuffer to have a pointer to a non const object? Well because I want to be able to pass it to any function as a parameter. All it does is to provide some usefull memory allocation functions, but basically it just functions as a normal pointer. I'm open for more of this kind of suggestions though, I didn't even think of efn's solution :-)
It basically goes back to what I said originally:  Both of those operators are so similiar, the compiler can't tell them apart.  You don't need both, so get rid of one or the other, and you should be fine.

I had classes that had both operators too, and when I switch to VS.NET, the compiler started to complain about them, so I removed the T* operator, and haven't had any issues.

But, it doesn't matter which one you get rid of, you basically have the same function twice.

Dex*
The compiler is right that your code is ambiguous.  You have two functions that can generate a pointer that can be subscripted.  The compiler doesn't have to be smart enough to see that the expression is on the left side of an assignment expression, so you probably want the one that points to non-constant characters.

You could specify unambiguously which function you want:

void Empty() throw()
{
    operator TCHAR*()[0] = '\0';
}

--efn
Avatar of LuCkY

ASKER

>> operator TCHAR*()[0] = '\0';

This solution would undermine my attempt to make it more readable.

>> No, protected inheritance means that clients of CString can't see CBuffer's members, but CString can still see all of CBuffer's public and protected members.

Does this mean that the above code wont compile, but this code will:

CString strString;
strString[0] = '\0';

I guess it does, makes sense too... :-) I think this is an interesting problem. It's one of those things were you have to choose between readability and 'workability'. The problem here is that I want CBuffer to use the non-const version of the operator, but CString to use the const version of the operator (for making it more safe). The easy solution would be to just use a non-const operator for CString as well and assume the programmer will not make the mistake of passing it to a function which might modify the buffer. Or I might make a const operator for CBuffer and give it a getPointer() function which returns a non const pointer. I guess I will do something like that.

I still find this an interesting question though so before I accept any answers I will wait a bit more for some constructive imput on how to work with such issues.
Keep in mind:

You can have BOTH a const and a non-const version.  The compiler (should) take the const version whenever it doesn't need a non-const.  Different situations will call for different things, and if you're making this class to use in the future, then you should plan ahead.  :)

D*
Avatar of LuCkY

ASKER

But if I have both a const and non-const version it will complain about an ambigious operator everytime I try to cast it to a non const parameter wont it? Why else would it complain now?
It shouldn't complain.

It is complaining now because you have 2 different non-const that both mean the same thing.

Dex*
> Does this mean that the above code wont compile, but this code will:

> CString strString;
> strString[0] = '\0';

> I guess it does, makes sense too...

No, I don't think this should compile.  If it's inside the class, operator [] is ambiguous; if it's outside the class, it can't use operator LPCTSTR because that is a pointer to const characters, and it can't use operator TCHAR* because that function is inaccessible due to the protected inheritance.

>  I want CBuffer to use the non-const version of the operator, but CString to use the const version of the operator (for making it more safe).

I think that's what you had in the last sample, if you can bring yourself to disambiguate the Empty code.

> ...assume the programmer will not make the mistake of passing it to a function which might modify the buffer.

Danger!

--efn
Avatar of LuCkY

ASKER

I see where this is going now indeed.

If I use this code:

template <typename T>
class CBuffer
{
public:
    operator T * () const throw()
    {
        return m_buffer;
    }

private:
    T * m_buffer;
};

class CString : protected CBuffer<TCHAR>
{
public:
    operator LPCTSTR() throw()
    {
        return (LPTSTR)(*this);
    }

    void Empty() throw()
    {
        (LPTSTR)(*this)[0] = '\0';
    }
};

Should do the trick then right? I can use the accessor inside the class by casting it to LPTSTR first and I can use it outside of the class without any special effort. I'll test it as soon as I'm able to :-)

>> No, I don't think this should compile.  If it's inside the class, operator [] is ambiguous; if it's outside the class, it can't use operator LPCTSTR because that is a pointer to const characters, and it can't use operator TCHAR* because that function is inaccessible due to the protected inheritance.

You are right, I ment in that case with a overloaded type casting operator too.
SOLUTION
Avatar of Dexstar
Dexstar

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of LuCkY

ASKER

Yes it should, but it doesn't...

template <typename T>
class CBuffer
{
public:
    operator T * () const throw()
    {
        return m_buffer;
    }

private:
    T * m_buffer;
};

class CString : protected CBuffer<TCHAR>
{
public:
    operator LPCTSTR() throw()
    {
        return (LPTSTR)(*this);
    }

    void Empty() throw()
    {
        ((LPTSTR)(*this))[0] = '\0';
    }
};

This part works fine, but if you compile this:

void main()
{
    CString strString;

    strString[0] = 'T';
    strString[1] = 'e';
    strString[2] = 's';
    strString[3] = 't';
    strString[4] = '\0';

    MessageBox(NULL, strString, NULL, 0);
}

It goes wrong. Even outside the class it still looks at the protected operator from CBuffer. I get the following error:

c:\Weltevreden Framework\Test Project\Main.cpp(10) : error C2593: 'operator [' is ambiguous
could be 'built-in C++ operator[(TCHAR , int)'
or       'built-in C++ operator[(LPCTSTR, int)'
while trying to match the argument list '(Weltevreden::Utility::CString, int)'

from which the first alternative is the CBuffer operator (I assume). So what's this? Desired compiler behaviour or not? So you guys have any more ideas? I -could- drop the operator from the CBuffer class and change it for a GetPointer() function, this would solve the problem, BUT I can't use a CBuffer type variable as a parameter anymore (which probably won't happen much anyway). And something I'd also still like is to be able to do some error checking on the [] operator.
ASKER CERTIFIED SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of DanRollins
MFC's CString class does not allow this syntax:
   strString[1]= 'T';
Maybe there is a reason :)

I've never worked on this type of problem before, but I found that I stopped getting errors when I added this to CBuffer:
      T& ElementAt( int nIdx ) { return m_buffer[nIdx]; }
and this to CString:
      TCHAR& operator[](int nIndex) { return CBuffer<TCHAR>::ElementAt(nIndex); }

But I don't really know why.  
I examined the MFC CObArray code for guidance.  The programmers' at Microsoft have encountered -- and solved -- all of these issues, many times, many years ago.

-- Dan
Avatar of LuCkY

ASKER

Actually I started using CBuffer as a member variable, but I started thinking that it would be a lot easier just to implement it to my class so I can use a lot of the base functions. For instance when the copy constructor gets called, all I have to do is to call the copy constructor of CBuffer on the initialization list.

>> I examined the MFC CObArray code for guidance.  The programmers' at Microsoft have encountered -- and solved -- all of these issues, many times, many years ago.

Yes I know I'm reinventing the wheel bigtime :-) Though sometimes if I look at the source code of Microsoft I really wonder why they took a certain approach.

Anyway I think this has lasted long enough now and I got a few nice idea's to work with. I guess I'll use it as a member variable again. Thanks a lot for all your help and usefull ideas :-)
Dan:

The reason CString doesn't allow direct access to the buffer (without the use of GetBuffer) is because it implements reference counting, and you can't/shouldn't change the buffer without it making a lazy copy first.

Dex*