Re: Automatic invocation of conversion function: operator std::ost
Paul wrote:
"Tom Widmer [VC++ MVP]" wrote:
So, again, the behaviour I expected was:
1) N::Log(N::ERR): a temporary Log object is created; its constructor
accepts, say, a single parameter indicating severity and locks the log file;
Ok.
2) the compiler discovers that the object finds itself to the left of ???<<???
operator that is not defined in this class;
Ok.
3) the compiler finds a conversion function and tries that: it returns
std::ostream& for which the operator is defined;
It only does that if it finds a non-member function which the operator
is defined. This conversion doesn't apply to member functions. The basic
sequence is:
Lookup all functions named << using ordinary lookup. This first searches
the class on the left for member operator<< functions, and then searches
for non-member operator<< functions.
Next, ADL is done to find some more non-member functions, based on the
namespaces of the arguments to the call.
Next, viable functions are found, e.g. ones where there are valid
conversion sequences to the parameters.
Finally, the best viable function is chosen, if there is one.
Your code is failing because only member operator<< functions of your
Log class are being found.
Not quite. According to Stroustrup (p. 267), "...in operator lookup no
preference is given to members over non-members. This differs from lookup of
named functions..."
I think you're misinterpreting that. Stroustrup is saying that ordinary
name lookup won't stop when a member operator is found (unlike for
functions, where ordinary name lookup will stop if the name is found as
an in scope member name). This doesn't mean that every class in
existence is searched for member operators whenever you use an operator,
only the class on the lhs is searched.
for
//inside, say, A::g():
a << b;
the compiler considers candidate functions:
a.operator<<(b);
operator<<(a, b); (current namespace and namespaces associated with the
arguments)
built in operator<<(a, b);
Contrast with:
//inside, say, A::g():
f();
Here, if f() is a member function, lookup halts immediately (though I
don't recall if associated namespaces are checked or not).
4) with output complete, the temporary object is destroyed and its
destructor unlocks the lock and releases the log file.
Ok.
namespace N {
// ...
std::ostream& operator <<(std::ostream& os, bool i)
{
return os.operator <<(i);
}
std::ostream& operator <<(std::ostream& os, const std::string& s)
{
return std::operator <<(os, s);
}
}
will actually make things work which gives me a glimmer of hope.
Now you have non-member functions, which will be found and added to the
overload set (assuming the functions are in scope at the call site).
One thing, I believe, I have established for sure: forgetting about the
std::ostream& operator<<(bool), which is a member-operator, and dealing
purely with the non-member std::ostream& operator <<(std::ostream&, const
std::string&), it is the addition of this non-member operator to the N
namespace (the namespace in which class Log is defined) that made the
automatic conversion from Log to std::ostream& with the help of the
conversion operator (operator std::ostream&()) work.
ADL on the log class means that namespace N will be searched. However,
namespace std will be searched too, since one of the arguments is
std::string. The problem is that although operator<< for std::string is
being found, template argument deduction fails, since the first
parameter requires a user defined conversion, which isn't allowed when
deducting template arguments.
In other words, the
compiler felt its presence within the elegible scope and performed the
conversion automatically. Where I am failing is understanding what is
required to bring << operators defined in std into the scope automatically
without redefining them (such as via forwarding functions). I even tried
adding Log to std as I described (namespaces being open) but somehow it still
did not work.
That doesn't help the problem. The reason your std::string forwarding
function solves the problem is because it is a non-template. For
example, adding this forwarding function to your original code works:
namespace std
{
ostream& operator<<(ostream& os, std::string const& s)
{
//call template version:
return std::operator<< <char, char_traits<char>, allocator<char>
>(os, s);
}
}
Because there is a fixed set of ostream::operator<< members, you can
easily add them all to your Log class, forwarding to the ostream
versions (just copy and paste them out of the basic_ostream class
definition).
Alternatives are:
*N::Log(N::ERR) << "Here goes the error message: integer = " << i
<< ", long = " << l << std::endl;
N::Log(N::ERR).get() << "Here goes the error message: integer = " << i
<< ", long = " << l << std::endl;
etc., where operator* and the function get() both return an ostream&.
The final alternative is a macro. e.g.
#define LOCKED_LOG(level) \
((mutex::scoped_guard(log_mutex),N::Log(level)))
with Log just a function returning an ostream&, and the mutex locking
handled by the separate scoped_guard object.
I did consider them all (with the only difference being the use of
overloaded () operator, solution that differed from the sought original by an
extra pair of round brackets: Log(INF)() << i ... - for your (*) and .get())
- but thank you for your suggestions. I am just a little stubborn.
Because of the way some templated operator<< functions are defined, you
need a non-template forwarding function. Try this:
namespace N
{
template <class T>
inline std::ostream& operator<<(Log const& l, T const& t)
{
return static_cast<std::ostream&>(l) << t;
}
//add overloads for ios_base&(*)(ios_base&) etc.
}
Tom