Zapobieganie przyjmowaniu przez funkcję const std :: string & od akceptacji 0

97

Warto tysiąc słów:

#include<string>
#include<iostream>

class SayWhat {
    public:
    SayWhat& operator[](const std::string& s) {
        std::cout<<"here\n"; // To make sure we fail on function entry
        std::cout<<s<<"\n";
        return *this;
    }
};

int main() {
    SayWhat ohNo;
    // ohNo[1]; // Does not compile. Logic prevails.
    ohNo[0]; // you didn't! this compiles.
    return 0;
}

Kompilator nie narzeka, gdy przekaże liczbę 0 operatorowi nawiasu akceptującemu ciąg znaków. Zamiast tego kompiluje się i kończy się niepowodzeniem przed wejściem do metody z:

terminate called after throwing an instance of 'std::logic_error'
  what():  basic_string::_S_construct null not valid

Na przykład:

> g++ -std=c++17 -O3 -Wall -Werror -pedantic test.cpp -o test && ./test
> g++ --version
gcc version 7.3.1 20180303 (Red Hat 7.3.1-5) (GCC)

Zgaduję że

Kompilator domyślnie korzysta z std::string(0)konstruktora, aby wprowadzić metodę, co daje ten sam problem (google powyższy błąd) bez uzasadnionego powodu.

Pytanie

Czy można to naprawić po stronie klasy, więc użytkownik interfejsu API tego nie odczuwa, a błąd jest wykrywany w czasie kompilacji?

Oznacza to dodanie przeciążenia

void operator[](size_t t) {
    throw std::runtime_error("don't");
}

nie jest dobrym rozwiązaniem.

kabanus
źródło
2
Skompilowany kod, zgłaszający wyjątek w Visual Studio ohNo [0] z wyjątkiem „0xC0000005: Lokalizacja odczytu naruszenia dostępu 0x00000000”
TruthSeeker
5
Zadeklaruj prywatne przeciążenie, operator[]()które przyjmuje intargument i nie definiuj go.
Peter
2
@Peter Chociaż zauważ, że to błąd linkera , który wciąż jest lepszy niż to, co miałem.
kabanus
5
@kabanus W powyższym scenariuszu wystąpi błąd kompilatora , ponieważ operator jest prywatny! Błąd linkera tylko jeśli zostanie wywołany w klasie ...
Aconcagua,
5
@Peter To szczególnie interesujące w sytuacjach, w których nie C ++ 11 jest dostępny - a te nie istnieją jeszcze dzisiaj (a właściwie jestem w projekcie mającym do czynienia, i brakuje mi dość dużo niektóre z nowych funkcji ... ).
Aconcagua,

Odpowiedzi:

161

Przyczyna std::string(0)jest prawidłowa, ponieważ 0jest stałą zerową wskaźnika. Zatem 0 odpowiada konstruktorowi ciągów, który przyjmuje wskaźnik. Następnie kod działa z warunkiem wstępnym, do którego nie można przekazać wskaźnika zerowego std::string.

Tylko literał 0byłby interpretowany jako stała wskaźnika zerowego, jeśli intbyłaby to wartość czasu działania w , nie miałbyś tego problemu (ponieważ wtedy rozdzielczość przeciążenia intszukałaby zamiast tego konwersji). Nie jest też dosłownie 1problemem, ponieważ 1nie jest stałą zerową wskaźnika.

Ponieważ jest to problem z czasem kompilacji (dosłowne nieprawidłowe wartości), można go złapać w czasie kompilacji. Dodaj przeciążenie tego formularza:

void operator[](std::nullptr_t) = delete;

std::nullptr_tjest rodzajem nullptr. I będzie on pasował do żadnego wskaźnika stałej zerowej, czy to 0, 0ULLczy nullptr. A ponieważ funkcja jest usunięta, spowoduje błąd czasu kompilacji podczas rozwiązywania przeciążenia.

StoryTeller - Unslander Monica
źródło
To zdecydowanie najlepsze rozwiązanie, zupełnie zapomniałem, że mogę przeciążać wskaźnik NULL.
kabanus
w Visual Studio nawet „ohNo [0]” zgłasza wyjątek o wartości zerowej. Czy oznacza to implementację specyficzną dla klasy std :: string?
TruthSeeker
@pmp Wyrzucane (jeśli cokolwiek) jest specyficzne dla implementacji, ale chodzi o to, że ciąg znaków jest wskaźnikiem NULL we wszystkich. Dzięki temu rozwiązaniu nie przejdziesz do części wyjątku, zostanie ona wykryta podczas kompilacji.
kabanus
18
@pmp - Przekazywanie wskaźnika pustego do std::stringkonstruktora nie jest dozwolone przez standard C ++. To nieokreślone zachowanie, więc MSVC może robić, co chce (na przykład zgłasza wyjątek).
StoryTeller - Unslander Monica,
26

Jedną z opcji jest zadeklarowanie privateprzeciążenia, operator[]()które akceptuje integralny argument, i nie definiuj go.

Ta opcja będzie działać ze wszystkimi standardami C ++ (od 1998 r.), W przeciwieństwie do takich opcji, void operator[](std::nullptr_t) = deletektóre są poprawne od C ++ 11.

Dzięki czemu operator[]()jest privateczłonkiem spowoduje diagnosable błąd na swoim przykładzie ohNo[0], chyba że wyrażenie jest używany przez funkcję składową lub friendklasy.

Jeśli to wyrażenie zostanie użyte z funkcji składowej lub friendklasy, kod zostanie skompilowany, ale - ponieważ funkcja nie jest zdefiniowana - generalnie kompilacja zakończy się niepowodzeniem (np. Błąd linkera z powodu niezdefiniowanej funkcji).

Piotr
źródło