Search code examples
c++11gccoperatorsinitializer-liststdset

Initializer_list initialization of std::set<my_class> with trivial operator<. Bug in gcc+ / standard library?


This is how my code looks like

#include <iostream>
#include <set>
using namespace std;

enum Enum_type 
{
    Enum_type_1 = 1,
    Enum_type_2,
    Enum_type_3,
    Enum_type_4
};

class my_class
{
public:

    my_class(Enum_type type) :
        source_type(type)
    {}

    bool operator<(const my_class &another) const;

    Enum_type source_type;
};

bool my_class::operator<(const my_class& another) const
{
    return true;
}

int main()
{
    std::set<my_class> bracers_initialized_set {
        my_class{Enum_type_1},
        my_class{Enum_type_2},
        my_class{Enum_type_3},
        my_class{Enum_type_4}};

    cout<< "bracers_initialized_set.size() " <<  bracers_initialized_set.size() <<endl;
    for (auto my_class_ : bracers_initialized_set) {
        cout << "enum value: " << my_class_.source_type << endl;
    }
    cout<< "bracers_initialized_set.size() " <<  bracers_initialized_set.size() <<endl;

    std::set<my_class> inserted_set;
    if (inserted_set.insert(my_class(Enum_type_1)).second) {
        cout << "inserted_1" << endl;
    }
    if (inserted_set.insert(my_class(Enum_type_2)).second) {
        cout << "inserted_2" << endl;
    }   
    if (inserted_set.insert(my_class(Enum_type_3)).second) {
        cout << "inserted_3" << endl;
    }
    if (inserted_set.insert(my_class(Enum_type_4)).second) {
        cout << "inserted_4" << endl;
    }

    cout<< "inserted_set.size() " <<  inserted_set.size() <<endl;
    for (auto my_class_ : inserted_set) {
        cout << "enum value: " << my_class_.source_type << endl;
    }
    cout<< "inserted_set.size() " <<  inserted_set.size() <<endl;
}

And this is the output:

bracers_initialized_set.size() 4
enum value: 2
enum value: 1
bracers_initialized_set.size() 4
inserted_1
inserted_2
inserted_3
inserted_4
inserted_set.size() 4
enum value: 4
enum value: 3
enum value: 2
enum value: 1
inserted_set.size() 4

As you can see the set initialized with initializer_list behaves bad (size is different than number of iterations). If I implement operator< this way:

bool my_class::operator<(const my_class& another) const
{
    return source_type<another.source_type;
}

then the output is correct for both examples.

However shouldnt the initializer_list initialization and insertion output be the same, regardless of how the operator< looks like? Also shouldnt size always reflect the number of elements?

$ gcc -v

COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.8/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 4.8.4-2ubuntu1~14.04' --with-bugurl=file:///usr/share/doc/gcc-4.8/README.Bugs --enable-languages=c,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.8 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.8 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --disable-libmudflap --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.8-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.8-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04) 

~Marcin


Solution

  • In the standard it's specified that, at §25.4/2:

    Compare is a function object type (20.9). The return value of the function call operation applied to an object of type Compare, when contextually converted to bool (Clause 4), yields true if the first argument of the call is less than the second, and false otherwise. Compare comp is used throughout for algorithms assuming an ordering relation. It is assumed that comp will not apply any non-constant function through the dereferenced iterator.

    (emphasis mine) and moreover at §25.4/3:

    For all algorithms that take Compare, there is a version that uses operator< instead. That is, comp(*i, *j) != false defaults to *i < *j != false. For algorithms other than those described in 25.4.3 to work correctly, comp has to induce a strict weak ordering on the values.

    (emphasis mine) which means, as described in §25.4/4:

    The term strict refers to the requirement of an irreflexive relation (!comp(x, x) for all x), and the term weak to requirements that are not as strong as those for a total ordering, but stronger than those for a partial ordering. If we define equiv(a, b) as !comp(a, b) && !comp(b, a), then the requirements are that comp and equiv both be transitive relations:

    • comp(a, b) && comp(b, c) implies comp(a, c)
    • equiv(a, b) && equiv(b, c) implies equiv(a, c) [ Note: Under these conditions, it can be shown that
      • equiv is an equivalence relation
      • comp induces a well-defined relation on the equivalence classes determined by equiv
      • The induced relation is a strict total ordering. — end note ]

    You are breaking those requirements, so the standard library is breaking its guarantees.