Ошибка преобразования Boost Variant при использовании посетителя

Я не имею в виду дамп кода, но это действительно самый маленький воспроизводимый пример, который я мог создать, даже после удаления всей логики, чтобы сделать его более понятным.

По сути, я пытаюсь реализовать свою собственную версию некоторых базовых типов в C++, чтобы имитировать динамическую типизацию, сохраняя типы в boost::variant с именем Values и используя boost::static_visitors для выполнения операций над вариантом Value. Одна операция, которую я пытаюсь реализовать, это оператор not equals, и для этого я создал посетителя с именем Not_Equal. Оператор Not_Equal использует SFINAE со структурой low_priority и high_priority, чтобы определить, разрешены ли два типа, используемые в операции.

Типы в варианте Values: {SpecialInt, SpecialBoolean, std::shared_ptr<SeriesInt>, std::shared_ptr<SeriesBoolean>}. Причина, по которой SeriesInt и SeriesBoolean являются интеллектуальными указателями, заключается в том, что они хранят много информации в моей реальной версии, поэтому их копирование будет дорогостоящим.

Допустимые операторы != следующие:

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

как представлено перегрузками операторов в каждом классе.

#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);
    }
};

Проблема возникает у посетителя в строке return a != b; , где типы операторов не являются общими_ptr и, следовательно, либо SpecialInt, либо SpecialBoolean, что вызывает ошибки:

Ошибка C2446 '!=': нет преобразования из 'SeriesInt *' в 'SeriesBoolean *'

Ошибка C2446 '!=': нет преобразования из 'SeriesBoolean *' в 'SeriesInt *'

Я не понимаю, какое это имеет отношение к SeriesBoolean* или SeriesInt*, поскольку он может принимать только типы SpecialInt и SpecialBoolean, но я заметил, что когда я удаляю конструкторы, которые принимают аргумент в SpecialInt и SpecialBoolean, код компилируется и запускается как обычно. Мне нужны эти конструкторы для загрузки значений в классы (логика удалена), поэтому мой вопрос: почему я получаю эти ошибки и как я могу это исправить?


person Tom    schedule 13.12.2020    source источник
comment
Трюк high_priority не является SFINAE (но больше похож на отправку тегов). Кроме того, вам это не нужно здесь, см. мой пример.   -  person sehe    schedule 14.12.2020


Ответы (1)


Конструкторы для ваших типов приводят к неоднозначным инициализаторам вариантов.

Если можете, подумайте о том, чтобы сделать их явными.

Кроме того, возвращаемые типы decltype на самом деле не имеют смысла, поскольку посетитель по определению возвращает Values.

Эта перегрузка

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

соответствует ВСЕМ комбинациям. operator != в таких случаях /не определено/. Вы хотели сделать:

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

Теперь вам также понадобится следующая перегрузка:

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

В противном случае два идентичных типа аргумента shared_pointer будут неоднозначными.

Эта перегрузка кажется неправильной:

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;
}

Это, очевидно, приведет к проблемам, потому что, например. сравните SeriesInt с SeriesBool, который не реализован. Поскольку его нет в вашем списке, отбросьте его.

Точно так же, поскольку

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

также соответствует, например, [ T = SeriesInt, U = SpecialBoolean ], он не будет компилироваться.

УПРОЩАТЬ!

Я бы в основном просмотрел список поддерживаемых перегрузок и просто явно реализовал их. Я буду использовать приведенные выше шаблоны только для случаев 1:1.

Обратите внимание, что последовательное (!) использование аргументов с помощью const& делает выполнение намного более эффективным, особенно для общих указателей.

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");
    }
};

Живая демонстрация

Жить на 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() {
}

БОНУС

Кроме того, я думаю, вам следует инкапсулировать оптимизацию использования shared_ptr‹›, исключив все ваши особые случаи.

Это упрощает все вышеперечисленное:

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");
    }
};

Вот полная демонстрация с тестами для этого 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";
    }
}

Печать

       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{...}   
person sehe    schedule 13.12.2020
comment
Кроме того, я думаю, вам следует инкапсулировать оптимизацию использования shared_ptr‹›, исключая все ваши особые случаи, в комплекте с 36 тестами - person sehe; 14.12.2020
comment
Как ни странно, с упрощениями вы можете внезапно просто SFINAE на a!=b: coliru. stacked-crooked.com/a/ed58cdebf04af047 - person sehe; 14.12.2020
comment
Большое спасибо за подробный ответ!! - person Tom; 14.12.2020
comment
Ваше здоровье. У меня была небольшая опечатка в реализации operator<<. исправлено и вариант Sfinae тоже - person sehe; 14.12.2020
comment
Замечено! Вы предложили много хороших изменений, которые значительно облегчают работу, например, инкапсуляцию файла shared_ptr. Не могу поверить, что я не подумал об этом. Просто вопрос, почему вы рекомендуете использовать явное для конструктора? - person Tom; 14.12.2020
comment
Это позволяет избежать неожиданных неявных преобразований. Вы не столкнетесь с этим легко после упрощений, но посетитель склонен к ним (поскольку первая перегрузка занимает (Values const&, Values const&), вызывая неожиданные конверсии. Примечание: вы можете избежать этой путаницы, отделив отправку вариантов от ваших перегрузок посетителя. Я столкнулся с что недавно здесь, где я также объясняю, как я обычно исправить / избежать этого. - person sehe; 14.12.2020
comment
Спасибо. Также вы упомянули, что для производительности лучше всего передавать std::shared_ptr как const&. Является ли это специфичным для shared_ptr (и других интеллектуальных указателей), потому что это не модуль и, следовательно, имеет дорогостоящие операции копирования? - person Tom; 14.12.2020
comment
благослови вас за всю помощь, которую вы оказали, я очень ценю это! - person Tom; 14.12.2020