views:

1054

answers:

5

When writing a class to act as a wrapper around a heap-allocated object, I encountered a problem with implicit type conversion that can be reduced to this simple example.

In the code below the wrapper class manages a heap-allocated object and implicitly converts to a reference to that object. This allows the wrapper object to be passed as the argument to the function write(...) since implicit conversion takes place. The compiler fails, however, when trying to resolve the call to operator<<(...), unless an explicit cast is made (checked with MSVC8.0, Intel 9.1 and gcc 4.2.1 compilers).

So, (1) why does the implicit conversion fail in this case? (2) could it be related to argument-dependent lookup? and (3) is there anything that can be done to make this work without the explicit cast?

#include <fstream>

template <typename T>
class wrapper
{
    T* t;
  public:
    explicit wrapper(T * const p) : t(p) { }
    ~wrapper() { delete t; }
    operator T & () const { return *t; }
};

void write(std::ostream& os)
{
    os << "(1) Hello, world!\n";
}

int main()
{
    wrapper<std::ostream> file(new std::ofstream("test.txt"));

    write(file);
    static_cast<std::ostream&>( file ) << "(2) Hello, world!\n";
    // file << "(3) This line doesn't compile!\n";
}
A: 

Check the signature of the insertion operator... I think they take non-const ostream reference?

Confirmed with C++03 standard, signature of the char* output operator is:

template<class charT, class traits>
basic_ostream<charT,traits>& operator<<(basic_ostream<charT,traits>&, const charT*);

which does indeed take a non-const reference. So your conversion operator does not match.

As noted in the comment: this is irrelevant.

There are various limits in the Standard about conversions applied... maybe this would need to implicit conversions (your operator, and cast to base type) when at most one should be applied.

Richard
Simon C.
I blame not enough C++ recently.
Richard
+1  A: 

The compiler doesn't have enough context to determine that operator& will make a valid conversion. So, yes, I think this is related to argument-dependent lookup: The compiler is looking for an operator<< that can accept a non-const wrapper<std::ostream> as its first parameter.

greyfade
+2  A: 

It fails because you're trying to resolve an operator of your wrapper<T> class that doesn't exist. If you want it to work without the cast, you could put together something like this:

template<typename X> wrapper<T> &operator <<(X &param) const {
    return t << param;
}

Unfortunately I don't know of a way to resolve the return type at compile time. Fortunately in most cases it's the same type as the object, including in this case with ostream.

EDIT: Modified code by suggestion from dash-tom-bang. Changed return type to wrapper<T> &.

zildjohn01
> to resolve the return type at compile timeThis would require more advanced template meta-programming... plus (likely) type trails plus SFINAE.
Richard
That might be a good workaround, but the following works even though the operator+= is not defined for wrapper<int>:wrapper<int> i(new int(10)); i += 1; // gives 11
Simon C.
The int case may function due to it being an integral type where the whole type promotion/conversion takes place more readily? E.g. if I'm +='ing an int, maybe it tries to turn the lhs to an int also? I like this response, it's probably the way to go although I'd probably return wrapper<T> instead
dash-tom-bang
Good call dash-tom-bang. I knew that :)
zildjohn01
The big difference is that += is an assignment operator.
Ismael
your op<< is now broken with returning wrapper<T>. op<< of the stream will return T return *this;
Johannes Schaub - litb
+1  A: 

I think the problem has to do with maintaining some compile-time constraints. In your example, the compiler first would have to find all the possible operator<<. Then, for each of them, it should try if your object could be automatically converted (directly or indirectly) to any of the types that each operator<< are able to accept.

This test can be really complex, and I think this is restricted to provide a reasonable compiling time.

Diego Sevilla
+1  A: 

After some testing, an even simpler example identifies the source of the problem. The compiler cannot deduce the template argument T in f2(const bar<T>&) below from implicit conversion of wrapper<bar<int> > to bar<int>&.

template <typename T>
class wrapper
{
    T* t;
  public:
    explicit wrapper(T * const p) : t(p) { }
    ~wrapper() { delete t; }
    operator T & () const { return *t; }
};

class foo { };

template <typename T> class bar { };

void f1(const foo& s) { }
template <typename T> void f2(const bar<T>& s) { }
void f3(const bar<int>& s) { }

int main()
{
    wrapper<foo> s1(new foo());
    f1(s1);

    wrapper<bar<int> > s2(new bar<int>());
    //f2(s2); // FAILS
    f2<int>(s2); // OK
    f3(s2);
}

In the original example, std::ostream is actually a typedef for the templated class std::basic_ostream<..>, and the same situation applies when calling the templated function operator<<.

Simon C.