Repository: xtensor-stack/xtensor (language: python)

Issue:
Issue when compiling with -D_GLIBCXX_DEBUG
If I compile xtensor with `-DCMAKE_CXX_FLAGS="-D_GLIBCXX_DEBUG" (macro described [here](https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_macros.html)) and try to run tests, some of them abort as such:
```
$ ./test_xarray
Running main() from .../xtensor/build/test/googletest-src/googletest/src/gtest_main.cc
[==========] Running 32 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 32 tests from xarray
[ RUN      ] xarray.shaped_constructor
[       OK ] xarray.shaped_constructor (0 ms)
[ RUN      ] xarray.strided_constructor
[       OK ] xarray.strided_constructor (0 ms)
[ RUN      ] xarray.valued_constructor
[       OK ] xarray.valued_constructor (0 ms)
[ RUN      ] xarray.strided_valued_constructor
[       OK ] xarray.strided_valued_constructor (0 ms)
[ RUN      ] xarray.xscalar_constructor
[       OK ] xarray.xscalar_constructor (0 ms)
[ RUN      ] xarray.copy_semantic
[       OK ] xarray.copy_semantic (0 ms)
[ RUN      ] xarray.move_semantic
[       OK ] xarray.move_semantic (0 ms)
[ RUN      ] xarray.resize
[       OK ] xarray.resize (0 ms)
[ RUN      ] xarray.reshape
[       OK ] xarray.reshape (0 ms)
[ RUN      ] xarray.transpose
[       OK ] xarray.transpose (0 ms)
[ RUN      ] xarray.transpose_row
[       OK ] xarray.transpose_row (0 ms)
[ RUN      ] xarray.access
[       OK ] xarray.access (0 ms)
[ RUN      ] xarray.unchecked
[       OK ] xarray.unchecked (0 ms)
[ RUN      ] xarray.at
[       OK ] xarray.at (1 ms)
[ RUN      ] xarray.element
[       OK ] xarray.element (0 ms)
[ RUN      ] xarray.indexed_access
[       OK ] xarray.indexed_access (0 ms)
[ RUN      ] xarray.broadcast_shape
[       OK ] xarray.broadcast_shape (0 ms)
[ RUN      ] xarray.iterator
[       OK ] xarray.iterator (0 ms)
[ RUN      ] xarray.fill
[       OK ] xarray.fill (0 ms)
[ RUN      ] xarray.initializer_list
[       OK ] xarray.initializer_list (0 ms)
[ RUN      ] xarray.zerod
/usr/include/c++/9/bits/stl_algo.h:3279:
In function:
    _FIter std::is_sorted_until(_FIter, _FIter, _Compare) [with _FIter = 
    const long unsigned int*; _Compare = std::less_equal<void>]

Error: comparison doesn't meet irreflexive requirements, assert(!(a < a)).

Objects involved in the operation:
    instance "functor" @ 0x0x7fffb67bbe4f {
      type = std::less_equal<void>;
    }
    iterator::value_type "ordered type" {
      type = unsigned long;
    }
Aborted (core dumped)
```
For reference, I'm on Ubuntu 20.04 with gcc 9.3.0.

I'm not sure if this is a bug or what. I discovered this via a user of OpenMC (which relies on xtensor), who [reported](https://openmc.discourse.group/t/tests-fail-when-openmc-is-built-with-d-glibcxx-debug-compiler-flag/981) this behavior.


Discussion / hints:
This is a problem with the usage of `std::less_equal<>()` as a [Compare](https://en.cppreference.com/w/cpp/named_req/Compare) function when used with `std::is_sorted` - [xreducer.hpp::299](https://github.com/xtensor-stack/xtensor/blob/78aaac39143caa78da7c5c0734ccef957535f0c0/include/xtensor/xreducer.hpp#L299), [xreducer.hpp::1308](https://github.com/xtensor-stack/xtensor/blob/78aaac39143caa78da7c5c0734ccef957535f0c0/include/xtensor/xreducer.hpp#L1308).

The comment provided with each of these lines is as follows:

```cpp
// using std::less_equal is counter-intuitive, but as the standard says (24.4.5):
// A sequence is sorted with respect to a comparator comp if for any iterator i pointing to the sequence and any non-negative integer n
// such that i + n is a valid iterator pointing to an element of the sequence, comp(*(i + n), *i) == false.
// Therefore less_equal is required to detect duplicates.
```
The following minimum working example ([godbolt](https://godbolt.org/z/c9jY1b1n6)) replicates this issue when compiled with `-D_GLIBCXX_DEBUG` and shows the above justification to be incorrect:

```cpp
#include <vector>
#include <functional>

auto main() -> int {
    std::vector<int> v{1, 5, 9, 3, 2, 9, 7};
    std::is_sorted(v.begin(), v.end(), std::less_equal<>());
    return EXIT_SUCCESS;
}
```
The provided excerpt is from [cppreference](https://en.cppreference.com/w/cpp/algorithm/is_sorted), and correctly states the requirement for the Compare function. However, it is misinterpreted to require using the `<=` binary predicate as the Compare function. When comparing elements in the sequence, one offset value will be `n==0`, and when the Compare function is the binary predicate `<=`, `comp(*(i + n), *i) == comp(*i , *i) == true`, which is a violation of the requirement of the Compare function as `comp(*(i+n), *i) != false` for all possible values of `n`.

More specifically, the Compare function is [required](https://eel.is/c++draft/alg.sorting.general#3.sentence-3) to implement [strict weak ordering](https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings). In fact, the [C++ working draft section on `[alg.sorting]` suggests sorting algorithms require an object that implements `operator<`](https://eel.is/c++draft/alg.sorting.general#1.sentence-1).

I believe that changing `std::less_equal<>()` to `std::less<>()` would resolve this issue. Evidence of this fix is here: https://godbolt.org/z/6f3sKchcz

Fix patch:
diff --git a/docs/source/numpy.rst b/docs/source/numpy.rst
index 5f5fc7699..6a7d0e1e4 100644
--- a/docs/source/numpy.rst
+++ b/docs/source/numpy.rst
@@ -421,19 +421,19 @@ hold any values and are computed upon access or assignment.
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
 |            Python 3 - numpy                                        |                C++ 14 - xtensor                                    |
 +====================================================================+====================================================================+
-| :any:`np.sum(a, axis=[0, 1]) <numpy.sum>`                          | ``xt::sum(a, {0, 1})``                                             |
+| :any:`np.sum(a, axis=(0, 1)) <numpy.sum>`                          | ``xt::sum(a, {0, 1})``                                             |
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
 | :any:`np.sum(a, axis=1) <numpy.sum>`                               | ``xt::sum(a, 1)``                                                  |
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
 | :any:`np.sum(a) <numpy.sum>`                                       | ``xt::sum(a)``                                                     |
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
-| :any:`np.prod(a, axis=[0, 1]) <numpy.prod>`                        | ``xt::prod(a, {0, 1})``                                            |
+| :any:`np.prod(a, axis=(0, 1)) <numpy.prod>`                        | ``xt::prod(a, {0, 1})``                                            |
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
 | :any:`np.prod(a, axis=1) <numpy.prod>`                             | ``xt::prod(a, 1)``                                                 |
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
 | :any:`np.prod(a) <numpy.prod>`                                     | ``xt::prod(a)``                                                    |
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
-| :any:`np.mean(a, axis=[0, 1]) <numpy.mean>`                        | ``xt::mean(a, {0, 1})``                                            |
+| :any:`np.mean(a, axis=(0, 1)) <numpy.mean>`                        | ``xt::mean(a, {0, 1})``                                            |
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
 | :any:`np.mean(a, axis=1) <numpy.mean>`                             | ``xt::mean(a, 1)``                                                 |
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
@@ -447,7 +447,7 @@ hold any values and are computed upon access or assignment.
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
 | :any:`np.trapz(a, x=b, axis=-1) <numpy.trapz>`                     | ``xt::trapz(a, b, -1)``                                            |
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
-| :any:`np.count_nonzero(a, axis=[0, 1]) <numpy.count_nonzero>`      | ``xt::count_nonzero(a, {0, 1})``                                   |
+| :any:`np.count_nonzero(a, axis=(0, 1)) <numpy.count_nonzero>`      | ``xt::count_nonzero(a, {0, 1})``                                   |
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
 | :any:`np.count_nonzero(a, axis=1) <numpy.count_nonzero>`           | ``xt::count_nonzero(a, 1)``                                        |
 +--------------------------------------------------------------------+--------------------------------------------------------------------+
diff --git a/include/xtensor/xreducer.hpp b/include/xtensor/xreducer.hpp
index d38fc73e0..ae0b962bc 100644
--- a/include/xtensor/xreducer.hpp
+++ b/include/xtensor/xreducer.hpp
@@ -292,13 +292,17 @@ namespace xt
         dynamic_shape<std::size_t> iter_shape = xtl::forward_sequence<dynamic_shape<std::size_t>, decltype(e.shape())>(e.shape());
         dynamic_shape<std::size_t> iter_strides(e.dimension());
 
-        // using std::less_equal is counter-intuitive, but as the standard says (24.4.5):
+        // std::less is used, because as the standard says (24.4.5):
         // A sequence is sorted with respect to a comparator comp if for any iterator i pointing to the sequence and any non-negative integer n
         // such that i + n is a valid iterator pointing to an element of the sequence, comp(*(i + n), *i) == false.
-        // Therefore less_equal is required to detect duplicates.
-        if (!std::is_sorted(axes.cbegin(), axes.cend(), std::less_equal<>()))
+        // Therefore less is required to detect duplicates.
+        if (!std::is_sorted(axes.cbegin(), axes.cend(), std::less<>()))
         {
-            XTENSOR_THROW(std::runtime_error, "Reducing axes should be sorted and should not contain duplicates");
+            XTENSOR_THROW(std::runtime_error, "Reducing axes should be sorted.");
+        }
+        if (std::adjacent_find(axes.cbegin(), axes.cend()) != axes.cend())
+        {
+            XTENSOR_THROW(std::runtime_error, "Reducing axes should not contain duplicates.");
         }
         if (axes.size() != 0 && axes[axes.size() - 1] > e.dimension() - 1)
         {
@@ -1301,13 +1305,17 @@ namespace xt
         , m_dim_mapping(xtl::make_sequence<dim_mapping_type>(typename O::keep_dims() ? m_e.dimension() : m_e.dimension() - m_axes.size(), 0))
         , m_options(std::forward<OX>(options))
     {
-        // using std::less_equal is counter-intuitive, but as the standard says (24.4.5):
+        // std::less is used, because as the standard says (24.4.5):
         // A sequence is sorted with respect to a comparator comp if for any iterator i pointing to the sequence and any non-negative integer n
         // such that i + n is a valid iterator pointing to an element of the sequence, comp(*(i + n), *i) == false.
-        // Therefore less_equal is required to detect duplicates.
-        if (!std::is_sorted(m_axes.cbegin(), m_axes.cend(), std::less_equal<>()))
+        // Therefore less is required to detect duplicates.
+        if (!std::is_sorted(m_axes.cbegin(), m_axes.cend(), std::less<>()))
+        {
+            XTENSOR_THROW(std::runtime_error, "Reducing axes should be sorted.");
+        }
+        if (std::adjacent_find(m_axes.cbegin(), m_axes.cend()) != m_axes.cend())
         {
-            XTENSOR_THROW(std::runtime_error, "Reducing axes should be sorted and should not contain duplicates");
+            XTENSOR_THROW(std::runtime_error, "Reducing axes should not contain duplicates.");
         }
         if (m_axes.size() != 0 && m_axes[m_axes.size() - 1] > m_e.dimension() - 1)
         {


Test patch:
diff --git a/test/test_xreducer.cpp b/test/test_xreducer.cpp
index b1708f65a..7f6f34454 100644
--- a/test/test_xreducer.cpp
+++ b/test/test_xreducer.cpp
@@ -1068,7 +1068,10 @@ namespace xt
     TEST(xreducer, double_axis)
     {
         xt::xarray<int> a = xt::ones<int>({ 3, 2});
-        XT_EXPECT_ANY_THROW(xt::sum(a, {1, 1}));
+        XT_EXPECT_NO_THROW(xt::sum(a, {0, 1})); // correct
+        XT_EXPECT_ANY_THROW(xt::sum(a, {0, 0})); // duplicate indices
+        XT_EXPECT_ANY_THROW(xt::sum(a, {1, 0})); // unordered indices
+        XT_EXPECT_ANY_THROW(xt::sum(a, {1, 1})); // duplicate indices
     }
 
     TEST(xreducer, sum_xtensor_of_fixed)