Search code examples
c++templateseigentemplate-specialization

Specializing std::less for Eigen::DenseBase


I need a total order on both Eigen::Matrix and Eigen::Array, therefore I would like to specialize std::less for the parent class of both: Eigen::DenseBase. From https://eigen.tuxfamily.org/dox/TopicFunctionTakingEigenTypes.html I know that I must define std::less for the templated type Eigen::DenseBase<Derived>. Passing templated function into template function (Eigen Derived) contains a working example for the Eigen::Matrix, so I adapted that for the parent class and came up with

template <typename Derived>
struct std::less<Eigen::DenseBase<Derived>> {
    bool operator()(const Eigen::DenseBase<Derived>& lhs,
                    const Eigen::DenseBase<Derived>& rhs) const {
        return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(),
                                            rhs.end());
    }
};

which does not work, because the compiler tries to use the default std::less, which uses operator<, which either does not exist (Eigen::Matrix) or does not return a single bool (Eigen::Array).

How to define less than (<) operator or std::less struct for Eigen::Vector3f? recommends not specializing std::less (when using a std::map), but to explicityly provide a custom compare struct, which works

struct eigenless {
    template <class Derived>
    bool operator()(Eigen::DenseBase<Derived> const& lhs,
                    Eigen::DenseBase<Derived> const& rhs) const {
        return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(),
                                            rhs.end());
    }
};

My question: How can I make a specialization of std::less work with Eigen::DenseBase? Here's a minimal example: https://godbolt.org/z/jMdGzf7hT


Solution

  • Disclaimer: this is not a complete answer but it's too long for mere comments.

    First issue is that the specialization of less is not in a std namespace or a namespace embedded into std so the specialization cannot be considered:
    from cppreferences

    This declaration must be in the same namespace or, for member templates, class scope as the primary template definition which it specializes.

    Second issue is that Eigen::Array3d inherits from Eigen::DenseBase<Derived> but is not Eigen::DenseBase<Derived>. Adding a specialization for Eigen::Array3d make it works as below, but its not what you want to achieve.

    #include <Eigen/Core>
    #include <map>
    
    namespace std {
    template <class Derived>
    struct less<Eigen::DenseBase<Derived>> {
        bool operator()(const Eigen::DenseBase<Derived>& lhs,
                        const Eigen::DenseBase<Derived>& rhs) const {
            return std::lexicographical_compare(lhs.cbegin(), lhs.cend(),
                                                rhs.cbegin(), rhs.cend());
        }
    };
    template <>
    struct less<Eigen::Array3d> {
        bool operator()(const Eigen::Array3d& lhs,
                        const Eigen::Array3d& rhs) const {
            return std::lexicographical_compare(lhs.cbegin(), lhs.cend(),
                                                rhs.cbegin(), rhs.cend());
        }
    };
    }  // namespace std
    
    // not specializing std::less, but using custom Compare, see
    // https://stackoverflow.com/questions/68321552
    struct eigenless {
        template <class Derived>
        bool operator()(Eigen::DenseBase<Derived> const& lhs,
                        Eigen::DenseBase<Derived> const& rhs) const {
            return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(),
                                                rhs.end());
        }
    };
    
    int main() {
        Eigen::Array3d a;
        Eigen::Array3d b;
        auto map1 = std::map<decltype(a), int>();
        map1[a] = 1;
        map1[b] = 2;
        auto map2 = std::map<decltype(a), int, eigenless>();
        map2[a] = 1;
        map2[b] = 2;
    }
    

    Live

    In your case, extending std with partial specialization seems to be allowed:
    cppreference:

    Adding template specializations
    Class templates
    It is allowed to add template specializations for any standard library class template to the namespace std only if the declaration depends on at least one program-defined type and the specialization satisfies all requirements for the original template, except where such specializations are prohibited.

    By the way, specializing in an unrelated namespace won't work.

    Regarding the inheritance issue, i'm beginning to think that it's not possible, at least without modifying the primary template parameters, which is not possible in this case.

    In your case, you can yet specialize for Eigen::Array (and Eigen::Matrix):

    #include <Eigen/Core>
    #include <map>
    
    namespace std {
    template <typename Scalar, int RowsAtCompileTime, int ColsAtCompileTime>
    struct less<Eigen::Array<Scalar, RowsAtCompileTime, ColsAtCompileTime>> {
        bool operator()(
            const Eigen::Array<Scalar, RowsAtCompileTime, ColsAtCompileTime>& lhs,
            const Eigen::Array<Scalar, RowsAtCompileTime, ColsAtCompileTime>& rhs)
            const {
            return std::lexicographical_compare(lhs.cbegin(), lhs.cend(),
                                                rhs.cbegin(), rhs.cend());
        }
    };
    // template <>
    // struct less<Eigen::Array3d> {
    //     bool operator()(const Eigen::Array3d& lhs,
    //                     const Eigen::Array3d& rhs) const {
    //         return std::lexicographical_compare(lhs.cbegin(), lhs.cend(),
    //                                             rhs.cbegin(), rhs.cend());
    //     }
    // };
    }  // namespace std
    
    // not specializing std::less, but using custom Compare, see
    // https://stackoverflow.com/questions/68321552
    struct eigenless {
        template <class Derived>
        bool operator()(Eigen::DenseBase<Derived> const& lhs,
                        Eigen::DenseBase<Derived> const& rhs) const {
            return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(),
                                                rhs.end());
        }
    };
    
    int main() {
        Eigen::Array3d a;
        Eigen::Array3d b;
        auto map1 = std::map<decltype(a), int>();
        map1[a] = 1;
        map1[b] = 2;
        auto map2 = std::map<decltype(a), int, eigenless>();
        map2[a] = 1;
        map2[b] = 2;
    }
    

    Live

    [EDIT] Eventually, a workaround is possible for the inheritance management, without modifying the primary template (which is not possible in this case). I don't find it satisfying but I give it anyway, for the record:

    #include <Eigen/Core>
    #include <map>
    #include <type_traits>
    
    template <typename T>
    struct EigenWrapper {
        static_assert(std::is_base_of<Eigen::DenseBase<T>, T>::value, "");
        using type = T;
        T const& val;
        EigenWrapper(T const& v) : val(v){};
    };
    
    namespace std {
    
    /// works only for 1D data, that have iterators
    template <typename T>
    struct less<EigenWrapper<T>> {
        bool operator()(EigenWrapper<T> const& lhs,
                        EigenWrapper<T> const& rhs) const {
            return std::lexicographical_compare(lhs.val.cbegin(), lhs.val.cend(),
                                                rhs.val.cbegin(), rhs.val.cend());
        }
    };
    
    }  // namespace std
    
    int main() {
        Eigen::Array3d a;
        Eigen::Array3d b;
        auto map1 = std::map<decltype(EigenWrapper(a)), int>();
        map1[EigenWrapper(a)] = 1;
        map1[EigenWrapper(b)] = 2;
    }
    

    Live
    In this snippet, I create a kind of proxy class for all classes derived from Eigen::DenseBase and I partially specialize std::less for these proxy. As you can see, it is not sufficient to manage both arrays and 1D data because only 1D data have an iterator interface in Eigen it seems. But you can keep the same technic to specialize even more (std::less specialization for EigenWrapper can now delegate its implementation to a user-defined functor that can be specialized according to the nature of the wrapped object).
    Yet in this case, its at least as much work as having a specialization for Eigen::Array and Eigen::Matrix. Besides client code is less readable as it must work with EigenWrapper objects instead of plain ones. With C++ lesser than 17, situation would be even worse as the EigenWrapper template argument could not be deduced from constructor. You would have to explicit it or use a creator function to have argument deduction.
    Nevertheless I found the technic worth mentioning because it can be applied to another class hierarchy.

    Hope it will help.