3
votes

I have

  • a class Value which can be construct with different types (Foo, Bar, int,...).

  • class Value should have common operations like <<, ==, <,... proceeded at the underlying type

  • I added the operator << outside class definition.

I have to the following code:

#include <iostream>

struct Foo {
};

struct Bar {
};

struct Value {
    template<typename T>
    Value(T) {
    }
};

std::ostream &operator<<(std::ostream &os, const Bar &) {
    return os << "Bar\n";
}

std::ostream &operator<<(std::ostream &os, const Value &value) {

    auto visitor = [&](auto a) -> decltype(os << a) {
        return os << a;
    };

    // Works
    visitor(Bar{});

    // Infinity call of this function with Value.
    visitor(Foo{});

    return os;
}

int main() {
    std::cout << Value(1);

    return 0;
}

Live example.

The problem: If the underlying type does not implements the operator<<, the operator<< from Value gets called recursive infinity. I want to get a compiler error like no match for call operator<<(std:ostream&, const Value&)... to use SFINAE with my visitor pattern (not shown here).

What I need is something like:

[&](auto a) -> std::enable_if_t<addressof?(os << a) != addressof?(os << Value{})>::value> {
    return os << a;
};

to disable this lambda, if the functions are the same. Is this possible?

Not valuable solutions:

  • make Value explicit
  • disable construction of Value with Foo
3

3 Answers

4
votes

You might add a wrapper to force only one conversion:

template <typename T>
struct OneConversion
{
    OneConversion(const T& t) : t(t) {}

    operator const T&() const {return t;}

    const T& t;  
};

template <typename T>
struct isOneConversion : std::false_type {};

template <typename T>
struct isOneConversion<OneConversion<T>> : std::true_type {};

struct Value {
    template<typename T, std::enable_if_t<!isOneConversion<T>::value>* = nullptr>
    Value(T) {}
};

std::ostream &operator<<(std::ostream &os, const Value &value) {

    auto visitor = [&](auto a) -> decltype(os << OneConversion<decltype(a)>(a)) {
        return os << OneConversion<decltype(a)>(a);
    };

    // Works
    visitor(Bar{});

    visitor(Foo{}); // Error as expected.

    return os;
}

Demo

2
votes

Without modifying the signature of std::ostream &operator<<(std::ostream &os, const Value &value) we can check if an attempt to call to operator<< for the type that a is deduced to in our lambda is well-formed:

auto visitor = [&](auto a) -> decltype(
                              static_cast<std::ostream&(*)(std::ostream&, const decltype(a)&)>(&operator<<)
                             (os, a)
                             )
{
    return os << a;
};

Works with Bar, fails with Foo with error message:

error: invalid static_cast from type '<unresolved overloaded function type>' to type 'std::ostream& (*)(std::ostream&, const Foo&

Demo

2
votes

You could replace

std::ostream &operator<<(std::ostream &os, const Value &value)
{
    // ...
}

with

template <typename T, typename = std::enable_if_t<std::is_same_v<T, Value>>>
std::ostream &operator<<(std::ostream &os, const T &value)
{
    // ...
}

You'll still be able to print Value objects with it, but without implicit conversions.

Putting it into your code makes it fail at visitor(Foo{}); with following error, which seems like what you want.

...
main.cpp:29:12: error: no match for call to '(operator<<(std::ostream&, const T&) [with T = Value; <template-parameter-1-2> = void; std::ostream = std::basic_ostream<char>]::<lambda(auto:1)>) (Foo)'
 visitor(Foo{});
 ~~~~~~~^~~~~~~
...