niedziela, 30 listopada 2014

Problem z operatorem trójelementowym

Parę dni temu C++ robił mi za kawę: podniósł ciśnienie bez łyka kofeiny. Poniższy kod:

#include <iostream>
#include <string>

std::ostream& operator<<(std::ostream& stream, const char* chr)
{
        stream << 0;
        return stream;
}

std::ostream& operator<<(std::ostream& stream, const std::string& str)
{
        stream << 1;
        return stream;
}

int main()
{
        std::cout << ( true ? "CCharStar" : std::string("StdString") ) << std::endl;
        std::cout << ( true ? std::string("StdString") : "CCharStar" ) << std::endl;

        return 0;
}
przeciąża operator strumienia dla łańcucha znaków oraz std::string. Zgodnie z pobieżną logiką pierwszym przypadku użycia operatora trójelementowego, z racji że warunek jest spełniony, winno wypisać "0", a w drugim "1".

To jest C++, więc nie może być łatwo. Niuans pokazany w tym kodzie był pośród setek innych linii, które posądzałem o nieprawidłowe działanie kodu. Po głębszej analizie tego, co powoduje błąd/niezrozumienie sprawa okazuje się całkiem prosta, ale serio mówiąc zajęło to trochę czasu.

W obu przypadkach zostanie wypisane "1", mimo że w pierwszym przypadku oczekiwałoby się 0. Przyczyną jest to, że operator trójpunktowy zawsze oczekuje, aby typ zwracany przez obie sekcje był identyczny, tym samym operator ?: nie jest wymienny z if-else. Tutaj są dwa różne typy, więc kompilator samowolnie szuka metody rzutowania jednego w drugi. Ponieważ std::string da się zbudować z const char*, to dochodzi do tego absurdu. Sytuacji by nie było, gdyby std::string miał konstruktor typu explicit, co oznacza niemożność niejawnego utworzenia zmiennej tego typu (np. poprzez operator trójelementowy).

Nie neguję słuszności tego mechanizmu a jedynie chcę się podzielić spostrzeżeniem, które - w bardziej 'życiowym' kodzie potrafi sprawić niemałą zagwozdkę, szczególnie w przypadku gdy programista nie jest zupełnie świadom, że oba typy użyte w tym operatorze powiązane są hierarchią dziedziczenia lub jeden da się zbudować z drugiego. Ani GCC, ani Clang (w domyślnej konfiguracji) nie wychwytują tego jako błędu. Następnym razem opiszę cyrk z tym operatorem gdy właczymy do tego dziedziczenie.

1 komentarz: