RPH - blog

Blog programisty

Błędy w implementacjach C++

Opublikowany: Niedziela 20.03.2016
1

W ostatnim tygodniu znalazłem dwa błędy w implementacjach C++. Mówiąc implementacja C++ mam na myśli i kompilator i bibliotekę standardową.

Znalezienie pierwszego z nich jest następstwem mojego błędu. Klasa umożliwiała dostęp do jednego ze swojch pól - sety przez dostęp do iteratorów. Kod był podobny do poniższego:

#include <set>

class SomeFunctionality
{
        typedef std::set<Something> SomethingSetT;
    public:
        typedef SomethingSetT::iterator SomethingSetIterT;


        SomethingSetIterT somethingSetBegin();
        SomethingSetIterT somethingSetEnd();
};

W pliku źródłowym znajdowały się odpowiednie definicje metod tej klasy. Rozszerzając funkcjonalność dodałem jedno pole, std::multiset i analogicznie funkcje do tego pierwszego pola. Tak więc po moich zmianach kod wyglądał tak:

#include <set>

class SomeFunctionality
{
        typedef std::set<Something> SomethingSetT;
        typedef std::multiset<Something> SomethingMultiSetT;
    public:
        typedef SomethingSetT::iterator SomethingSetIterT;
        typedef SomethingMultiSetT::iterator SomethingMultiSetIterT;


        SomethingSetIterT somethingSetBegin();
        SomethingSetIterT somethingSetEnd();

        SomethingMultiSetIterT somethingMultiSetBegin();
        SomethingMultiSetIterT somethingMultiSetEnd();
};

Następnie popełniłem błąd. Definiując nowe metody w pliku źródłowym skopiowałem to co już było i zmieniałem tylko nazwy. Niestety zapomniałem zmienić typ zwracany przez funkcję. Mój kod wyglądał tak:

SomeFunctionality::SomethingSetIterT
SomeFunctionality::somethingMultiSetBegin()
{
    return mSomethingMultiSet.begin();
}

SomeFunctionality::SomethingSetIterT
SomeFunctionality::somethingMultiSetBegin()
{
    return mSomethingMultiSet.end();
}

Co więcej na moim środowisku programistycznym (Debian z g++ 4.9.3) to wszystko się kompilowało bez błędu i działało bez zarzutu. Kompilator nawet nie wyświetlił ostrzeżenia, o czym na pewno dowiedziałbym się, ponieważ projekt kompilowany jest z opcją która wskazuje że ostrzeżenia należy traktować jak błędy (-Werror). O błędzie dowiedziałem się następnego dnia - kod nie kompilował się na Visual Studio 2010, wyświetlając informację że przeciążona metoda różni się tylko wartością wzracaną. O dziwo nowsza wersja, Visual Studio, 2013 skompilowała kod.

Błąd udało mi się porawić szybko, jednak trochę dłużej zastanawiałem się dlaczego kompilatory potrafiły go skompilować. Pierwszą rzeczą którą zrobiłem było poszukanie informacji w kodzie źródłowym biblioteki standardowej. Okazało się że std::set oraz std::multiset są fasadą do drzewa czerwono-czarnego. Co więcej odpowiednie metody begin() oraz end() zwracają iteratory do tego drzewa, zatem mają ten sam typ. Dlatego kompilator potrafił to skompilować i program działał prawidłowo. W skrócie kod std::set i std::multiset wygląda podobnie:

template<typename _Key, typename _Compare = std::less<_Key>,
    typename _Alloc = std::allocator<_Key> >
class set
{
    private:
        typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template
            rebind<_Key>::other _Key_alloc_type;

        typedef _Rb_tree<key_type, value_type, _Identity<value_type>,
            key_compare, _Key_alloc_type> _Rep_type;

        _Rep_type _M_t;  // Red-black tree representing set.

    public:
        typedef typename _Rep_type::const_iterator            iterator;

        // ...

        iterator
        begin() const _GLIBCXX_NOEXCEPT
        { return _M_t.begin(); }

        iterator
        end() const _GLIBCXX_NOEXCEPT
        { return _M_t.end(); }

        // ...
};

Zapewne, w Visual Studio 2010, iteratory dla std::set i std::multiset są innego typu. Nie wiem czy jest to błąd implementacji, czy może też Standard pozwala lub nie określa tego, ale w Visual Studio 2013 zostało to zmienione tak, aby kod kompilował się podobnie jak w g++. Gdy znajdę trochę czasu poszukam informacji na ten temat.

Drugi znaleziony błąd jest już jednoznaczny, Visual Studio nie zastosowało się do Standardu C++, chociaż jest prawdopodobne aby programiści z Microsoftu chcieli naciągnąć Standard :)

Błąd polega na użyciu wewnętrznie zadeklarowanego typu jako parametru szablonu. Przykład:

#include <vector>

int someFunc()
{
    struct Param
    {
        int a;
        int b;
    };

    std::vector<Param> params;

    // ...

Visual Studio skompiluje poniższy kod, ale g++ wyświetli następujące błędy:

t.cpp: In function ‘int someFunc()’:
t.cpp:11:17: error: template argument for ‘template<class> class std::allocator’ uses local type ‘someFunc()::Kod’
  std::vector<Kod> agg;
                 ^
t.cpp:11:17: error:   trying to instantiate ‘template<class> class std::allocator’
t.cpp:11:17: error: template argument 2 is invalid

Dlaczego ten kod jest niepoprawny wyraźnie mówi Standard:

A local type, a type with no linkage, an unnamed type or a type compounded from any of these types shall not be used as a template-argument for a template type-parameter.

W Visual Studio jest dużo więcej błędów w implementacji C++ jednak w tym tygodniu napotkałem na tylko ten jeden. Z tych dwóch przedstawionych błędów ten pierwszy jest najbardziej ciekawy i postaram się dowiedzieć czym to jest spowodowane.


Komentarze

Poniedziałek 21.03.2016 15:32 Programers pisze:

Dlatego między innymi osobiście zrezygnowałem z Visual Studio na rzecz innych edytorów. Pozdrawiam

Zostaw komentarz