Question

Here's a simple program using Boost Test which behaves "strangely":

#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE foo
#include <boost/test/unit_test.hpp>

class C
{
public:
  C(char* str) : m_str(str) {}
  operator char*() const { return m_str; }
  char* get() const { return m_str; }

private:
  char* m_str;
};


BOOST_AUTO_TEST_CASE(test1)
{
  char s1[] = "hello";
  char s2[] = "hello";
  C c1(s1);
  C c2(s2);
  BOOST_CHECK_EQUAL(s1, s2); // check 1: passes
  BOOST_CHECK_EQUAL(c1, c2); // check 2: fails (why?)
  BOOST_CHECK_EQUAL(c1.get(), c2.get()); // check 3: passes
}

If you run this it will report a failure when comparing c1 and c2, when it seems like it should pass. The reason is this code inside Boost Test (I'm using 1.51):

// this is called for check 2
template <class Left, class Right>
predicate_result equal_impl( Left const& left, Right const& right )
{
    return left == right;
}

// this is called for checks 1 and 3
predicate_result        BOOST_TEST_DECL equal_impl( char const* left, char const* right );
inline predicate_result equal_impl( char* left, char* right )       { return equal_impl( static_cast<char const*>(left), static_cast<char const*>(right) ); }

// this decides which comparator to call
struct equal_impl_frwd {
    // this is called for checks 2 and 3
    template <typename Left, typename Right>
    inline predicate_result
    call_impl( Left const& left, Right const& right, mpl::false_ ) const
    {
        return equal_impl( left, right );
    }

    // this is called for check 1
    template <typename Left, typename Right>
    inline predicate_result
    call_impl( Left const& left, Right const& right, mpl::true_ ) const
    {
        return (*this)( right, &left[0] );
    }

    template <typename Left, typename Right>
    inline predicate_result
    operator()( Left const& left, Right const& right ) const
    {
        typedef typename is_array<Left>::type left_is_array;
        return call_impl( left, right, left_is_array() );
    }
};

So first, BOOST_CHECK_EQUAL decides at compile time whether the arguments are arrays. In check 1 they are, and the arrays are degraded to pointers. Then it decides how to compare the arguments. If the arguments are of type char*, it compares them as C strings. Otherwise, it uses operator ==. So the issue it that class C is not char*, so check 2 is done using operator ==. But there is no operator == for class C, so the compiler decides to implicitly convert c1 and c2 to char*, at which point operator == is defined, but compares them as addresses and not as C strings.

So we end up with a rather weird situation: Boost Test intended to always compare char* arguments as C strings, but it didn't know the only way to compare c1 and c2 was to convert them to char*.

My question is, how can we do better here? For example, is there a way to understand at compile time that when operator == is called for c1 and c2 that it will be using their implicit conversion to char*? This is sort of like using decltype() to figure out the return type of an expression at compile time, except that we need to figure out the argument types of an expression (namely c1 == c2).

Was it helpful?

Solution

I don't think you can really support this scenario because all SFINAE techniques I can think of would run into ambiguous overloads.

This is, in fact, precisely the documented limitation with Boost's has_equal_to<A,B,R> type trait:

There is an issue if the operator exists only for type A and B is convertible to A. In this case, the compiler will report an ambiguous overload.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top