c++boostoperator-overloadingsfinaestatic-visitor

Boost Variant conversion error when using visitor


I don't mean to code dump but this is genuinely the smallest reproducible example of this I could create even after removing all the logic to make it clearer.

Essentially I'm trying to implement my own version of some of the base types in C++ to mimic dynamic typing by storing the types in a boost::variant called Values and using boost::static_visitors to perform operations on the Value variant. One operation I'm trying to implement is the the not equals operator, and I have created a visitor called Not_Equal to achieve this. The Not_Equal operator uses SFINAE with a low_priority and high_priority struct to determine if the two types used in the operation are allowed.

The types in the Values variant are: {SpecialInt, SpecialBoolean, std::shared_ptr<SeriesInt>, std::shared_ptr<SeriesBoolean>}. The reason why the SeriesInt and SeriesBoolean are smart pointers is because they store a lot of information in my real version so copying them will be expensive.

The accepted != operators are as follows:

SpecialInt != SpecialInt
SpecialBoolean != SpecialBoolean
SeriesInt != SpecialInt
SeriesInt != SeriesInt
SeriesBoolean != SeriesBoolean

as represented by the operator overloads in each class.

#include <memory>
#include <boost/variant/static_visitor.hpp>
#include <boost/variant.hpp>

class SpecialBoolean  {
public:

    SpecialBoolean(bool val) {} //removing this line fixes it
    SpecialBoolean()  {}

    SpecialBoolean operator!= (const SpecialBoolean& rhs) const {
        return *this;
    }

};

class SpecialInt {
public:

    SpecialInt(float val) {} //removing this line fixes it
    SpecialInt() {}

    SpecialBoolean operator!= (const SpecialInt& rhs) const {
        return SpecialBoolean();
    }

};

class SeriesBoolean {
public:

    SeriesBoolean() {}

    std::shared_ptr<SeriesBoolean> operator!= (const SpecialBoolean& rhs) const {
        return std::make_shared<SeriesBoolean>();
    }
    std::shared_ptr<SeriesBoolean> operator!= (const SeriesBoolean& rhs) const {
        return std::make_shared<SeriesBoolean>();
    }

};

class SeriesInt {
public:
    
    SeriesInt() {}
    
    std::shared_ptr<SeriesBoolean> operator!= (const SpecialInt& rhs) const {
        return std::make_shared<SeriesBoolean>();
    }
    std::shared_ptr<SeriesBoolean> operator!= (const SeriesInt& rhs) const {
        return std::make_shared<SeriesBoolean>();
    }

};

typedef boost::variant <SpecialInt, SpecialBoolean, std::shared_ptr<SeriesInt>, std::shared_ptr<SeriesBoolean> > Values;

struct low_priority {};
struct high_priority : low_priority {};

struct Not_Equal : public boost::static_visitor<Values> {

    auto operator() (Values const& a, Values const& b) const {
        return boost::apply_visitor(*this, a, b);
    }

    template <typename T, typename U>
    auto operator() (high_priority, T a, U b) const -> decltype(Values(a != b)) {
        return a != b; // problem here
    }

    template <typename T, typename U>
    auto operator() (high_priority, std::shared_ptr<T> a, std::shared_ptr<U> b) const -> decltype(Values(*a != *b)) {
        return *a != *b;
    }

    template <typename T, typename U>
    auto operator() (high_priority, std::shared_ptr<T> a, U b) const -> decltype(Values(*a != b)) {
        return *a != b;
    }

    template <typename T, typename U>
    Values operator() (low_priority, T, U) const {
        throw std::runtime_error("Incompatible arguments");
    }

    template <typename T, typename U>
    Values operator() (T a, U b) const {
        return (*this)(high_priority{}, a, b);
    }
};

The problem occurs in the visitor at line return a != b; , where the operator types are not shared_ptr's and therefore either SpecialInt or SpecialBoolean which causes the errors:

Error C2446 '!=': no conversion from 'SeriesInt *' to 'SeriesBoolean *'

Error C2446 '!=': no conversion from 'SeriesBoolean *' to 'SeriesInt *'

I don't understand what it has to do with SeriesBoolean* or SeriesInt* since it can only accept types of SpecialInt and SpecialBoolean, but I've noticed that when I remove the constructors which take a argument in SpecialInt and SpecialBoolean that the code compiles and runs as normal. I need those constructors to load values into the classes (logic removed) so my question is why am I getting these errors and how can I fix this?


Solution

  • The contructors for your types lead to ambiguous variant initializers.

    If you can, consider making them explicit.

    Also, the decltype return types don't really make sense since the visitor returns Values by definition.

    This overload

    template <typename T, typename U>
    Values operator()(high_priority, T a, U b) const {
        return a != b; // problem here
    }
    

    matches ALL combinations. operator != is /not defined/ in such cases. Did you mean to do:

    template <typename T>
    Values operator()(high_priority, T const& a, T const& b) const {
        return a != b; // problem here
    }
    

    Now you need the following overload as well:

    template <typename T>
    Values operator()(high_priority, std::shared_ptr<T> const& a, std::shared_ptr<T> const& b) const {
        return *a != *b;
    }
    

    Otherwise two identical shared_pointer argument types would be ambiguous.

    This overload seems wrong:

    template <typename T, typename U>
    Values operator()(high_priority, std::shared_ptr<T> const& a, std::shared_ptr<U> const& b) const {
        return *a != *b;
    }
    

    This will obviously lead to problems because it will e.g. compare SeriesInt to SeriesBool which isn't implemented. Since it's not on your list, drop it.

    Likewise, since

    template <typename T, typename U>
    Values operator()(high_priority, std::shared_ptr<T> const& a, U const& b) const {
        return *a != b;
    }
    

    also matches e.g [ T = SeriesInt, U = SpecialBoolean ], it will not compile.

    SIMPLIFY!

    I would basically run down the list of supported overloads and just explicitly implement them. I'll use the above templates only for the cases that are 1:1.

    Note that consistently (!) taking args by const& make the execution a lot more efficient especially for the shared pointers.

    struct Not_Equal : boost::static_visitor<Values> {
        Values operator()(Values const& a, Values const& b) const {
            return boost::apply_visitor(*this, a, b);
        }
    
        // SpecialInt     != SpecialInt
        // SpecialBoolean != SpecialBoolean
        template <typename T>
        Values operator()(T const& a, T const& b) const {
            return a != b;
        }
    
        // SeriesInt      != SeriesInt
        // SeriesBoolean  != SeriesBoolean
        template <typename T>
        Values operator()(std::shared_ptr<T> const& a, std::shared_ptr<T> const& b) const {
            return *a != *b;
        }
    
        // SeriesInt      != SpecialInt
        Values operator()(std::shared_ptr<SeriesInt> const& a, SpecialInt const& b) const {
            return *a != b;
        }
    
        template <typename... T>
        Values operator()(T const&...) const {
            throw std::runtime_error("Incompatible arguments");
        }
    };
    

    Live Demo

    Live On Coliru

    #include <boost/variant.hpp>
    #include <boost/variant/static_visitor.hpp>
    #include <memory>
    
    struct SpecialBoolean {
        explicit SpecialBoolean(bool /*val*/ = false) {}
    
        SpecialBoolean operator!=(const SpecialBoolean& /*rhs*/) const { return *this; }
    };
    
    struct SpecialInt {
        explicit SpecialInt(float /*val*/ = 0) {}
    
        SpecialBoolean operator!=(const SpecialInt& /*rhs*/) const {
            return SpecialBoolean();
        }
    };
    
    struct SeriesBoolean {
        SeriesBoolean() {}
    
        std::shared_ptr<SeriesBoolean> operator!=(const SpecialBoolean& /*rhs*/) const {
            return std::make_shared<SeriesBoolean>();
        }
        std::shared_ptr<SeriesBoolean> operator!=(const SeriesBoolean& /*rhs*/) const {
            return std::make_shared<SeriesBoolean>();
        }
    };
    
    struct SeriesInt {
        SeriesInt() {}
    
        std::shared_ptr<SeriesBoolean> operator!=(const SpecialInt& /*rhs*/) const {
            return std::make_shared<SeriesBoolean>();
        }
        std::shared_ptr<SeriesBoolean> operator!=(const SeriesInt& /*rhs*/) const {
            return std::make_shared<SeriesBoolean>();
        }
    };
    
    typedef boost::variant<
        SpecialInt,
        SpecialBoolean,
        std::shared_ptr<SeriesInt>,
        std::shared_ptr<SeriesBoolean>
      >
      Values;
    
    struct Not_Equal : boost::static_visitor<Values> {
        Values operator()(Values const& a, Values const& b) const {
            return boost::apply_visitor(*this, a, b);
        }
    
        // SpecialInt     != SpecialInt
        // SpecialBoolean != SpecialBoolean
        template <typename T>
        Values operator()(T const& a, T const& b) const {
            return a != b;
        }
    
        // SeriesInt      != SeriesInt
        // SeriesBoolean  != SeriesBoolean
        template <typename T>
        Values operator()(std::shared_ptr<T> const& a, std::shared_ptr<T> const& b) const {
            return *a != *b;
        }
    
        // SeriesInt      != SpecialInt
        Values operator()(std::shared_ptr<SeriesInt> const& a, SpecialInt const& b) const {
            return *a != b;
        }
    
        template <typename... T>
        Values operator()(T const&...) const {
            throw std::runtime_error("Incompatible arguments");
        }
    };
    
    int main() {
    }
    

    BONUS

    Also, I think you should encapsulate the optimization of using shared_ptr<>, eliminating all your special cases.

    This simplifies all of the above to:

    struct Not_Equal : boost::static_visitor<Values> {
        Values operator()(Values const& a, Values const& b) const {
            return boost::apply_visitor(*this, a, b);
        }
    
        template <typename T>
        Values operator()(T const& a, T const& b) const { return a != b; }
        Values operator()(SeriesInt const& a, SpecialInt const& b) const { return a != b; }
        Values operator()(SeriesBoolean const& a, SpecialBoolean const& b) const { return a != b; }
    
        template <typename... T>
        Values operator()(T const&...) const {
            throw std::runtime_error("Incompatible arguments");
        }
    };
    

    Here's a complete demo with testcases for that Live On Coliru

    #include <boost/variant.hpp>
    #include <boost/variant/static_visitor.hpp>
    #include <memory>
    #include <vector>
    #include <iostream>
    #include <iomanip>
    
    struct SpecialBoolean {
        explicit SpecialBoolean(bool val = false) : val(val) {}
        SpecialBoolean operator!=(const SpecialBoolean& rhs) const { return SpecialBoolean{val != rhs.val}; }
      private:
        bool val;
        friend std::ostream& operator<<(std::ostream& os, SpecialBoolean const& b) { return os << "SpecialBoolean{" << std::boolalpha << b.val << "}"; }
    };
    
    struct SpecialInt {
        explicit SpecialInt(float val = false) : val(val) {}
        SpecialBoolean operator!=(const SpecialInt& rhs) const { return SpecialBoolean{val != rhs.val}; }
      private:
        float val;
        friend std::ostream& operator<<(std::ostream& os, SpecialInt const& i) { return os << "SpecialInt{" << i.val << "}"; }
    };
    
    struct SeriesBoolean {
        SeriesBoolean operator!=(const SpecialBoolean& /*rhs*/) const { return {}; } // TODO
        SeriesBoolean operator!=(const SeriesBoolean& /*rhs*/) const { return {}; } // TODO
    
      private:
        struct VeryLarge {
            std::array<SpecialBoolean, 512> _it_is;
        };
        std::shared_ptr<VeryLarge> _data = std::make_shared<VeryLarge>();
        friend std::ostream& operator<<(std::ostream& os, SeriesBoolean const&) { return os << "SeriesBoolean{...}"; }
    };
    
    struct SeriesInt {
        SeriesBoolean operator!=(const SpecialInt& /*rhs*/) const { return {}; }
        SeriesBoolean operator!=(const SeriesInt& /*rhs*/) const { return {}; }
    
      private:
        struct VeryLarge {
            std::array<SpecialInt, 512> _it_is;
        };
        std::shared_ptr<VeryLarge> _data = std::make_shared<VeryLarge>();
        friend std::ostream& operator<<(std::ostream& os, SeriesInt const&) { return os << "SeriesInt{...}"; }
    };
    
    using Values = boost::variant< SpecialInt, SpecialBoolean, SeriesInt, SeriesBoolean >;
    
    struct Not_Equal : boost::static_visitor<Values> {
        Values operator()(Values const& a, Values const& b) const {
            return boost::apply_visitor(*this, a, b);
        }
    
        template <typename T>
        Values operator()(T const& a, T const& b) const { return a != b; }
        Values operator()(SeriesInt const& a, SpecialInt const& b) const { return a != b; }
        Values operator()(SeriesBoolean const& a, SpecialBoolean const& b) const { return a != b; }
    
        template <typename... T>
        Values operator()(T const&...) const {
            throw std::runtime_error("Incompatible arguments");
        }
    };
    
    int main() {
        Values const vv[] = {
            SpecialInt(42),
            SpecialInt(-314e-2),
            SpecialBoolean(false),
            SpecialBoolean(true),
            SeriesInt(),
            SeriesBoolean()
        };
    
        Not_Equal const neq;
    
        auto col = [](auto const& v, bool right = false) -> auto& {
            std::ostringstream ss; // just for quick formatting
            ss << v;
    
            if (right)
                std::cout << std::right;
            else
                std::cout << std::left;
            return std::cout << std::setw(21) << ss.str();
        };
    
        for (auto const& a: vv) for (auto const& b: vv) try {
            col(a, true) << " != ";
            col(b) << " --> ";
            col(neq(a, b)) << "\n";
        } catch(std::exception const& e) {
            col(e.what()) << "\n";
        }
    }
    

    Printing

           SpecialInt{42} != SpecialInt{42}        --> SpecialBoolean{false}
           SpecialInt{42} != SpecialInt{-3.14}     --> SpecialBoolean{true} 
           SpecialInt{42} != SpecialBoolean{false} --> Incompatible arguments
           SpecialInt{42} != SpecialBoolean{true}  --> Incompatible arguments
           SpecialInt{42} != SeriesInt{...}        --> Incompatible arguments
           SpecialInt{42} != SeriesBoolean{...}    --> Incompatible arguments
        SpecialInt{-3.14} != SpecialInt{42}        --> SpecialBoolean{true} 
        SpecialInt{-3.14} != SpecialInt{-3.14}     --> SpecialBoolean{false}
        SpecialInt{-3.14} != SpecialBoolean{false} --> Incompatible arguments
        SpecialInt{-3.14} != SpecialBoolean{true}  --> Incompatible arguments
        SpecialInt{-3.14} != SeriesInt{...}        --> Incompatible arguments
        SpecialInt{-3.14} != SeriesBoolean{...}    --> Incompatible arguments
    SpecialBoolean{false} != SpecialInt{42}        --> Incompatible arguments
    SpecialBoolean{false} != SpecialInt{-3.14}     --> Incompatible arguments
    SpecialBoolean{false} != SpecialBoolean{false} --> SpecialBoolean{false}
    SpecialBoolean{false} != SpecialBoolean{true}  --> SpecialBoolean{true} 
    SpecialBoolean{false} != SeriesInt{...}        --> Incompatible arguments
    SpecialBoolean{false} != SeriesBoolean{...}    --> Incompatible arguments
     SpecialBoolean{true} != SpecialInt{42}        --> Incompatible arguments
     SpecialBoolean{true} != SpecialInt{-3.14}     --> Incompatible arguments
     SpecialBoolean{true} != SpecialBoolean{false} --> SpecialBoolean{true} 
     SpecialBoolean{true} != SpecialBoolean{true}  --> SpecialBoolean{false}
     SpecialBoolean{true} != SeriesInt{...}        --> Incompatible arguments
     SpecialBoolean{true} != SeriesBoolean{...}    --> Incompatible arguments
           SeriesInt{...} != SpecialInt{42}        --> SeriesBoolean{...}   
           SeriesInt{...} != SpecialInt{-3.14}     --> SeriesBoolean{...}   
           SeriesInt{...} != SpecialBoolean{false} --> Incompatible arguments
           SeriesInt{...} != SpecialBoolean{true}  --> Incompatible arguments
           SeriesInt{...} != SeriesInt{...}        --> SeriesBoolean{...}   
           SeriesInt{...} != SeriesBoolean{...}    --> Incompatible arguments
       SeriesBoolean{...} != SpecialInt{42}        --> Incompatible arguments
       SeriesBoolean{...} != SpecialInt{-3.14}     --> Incompatible arguments
       SeriesBoolean{...} != SpecialBoolean{false} --> SeriesBoolean{...}   
       SeriesBoolean{...} != SpecialBoolean{true}  --> SeriesBoolean{...}   
       SeriesBoolean{...} != SeriesInt{...}        --> Incompatible arguments
       SeriesBoolean{...} != SeriesBoolean{...}    --> SeriesBoolean{...}