Nie­spój­ne i my­lą­ce kon­struk­to­ry w std::string

• tech • 753 słowa • 4 mi­nu­ty czy­ta­nia

Kie­dyś czę­sto pro­ble­mem były nie­jaw­ne kon­wer­sje mię­dzy ty­pa­mi w C++, które szcze­gól­nie uwi­dacz­nia­ły się przy kon­struk­to­rach kon­wer­sji… Kto by po­my­ślał, że dziś nadał try­wial­ne błędy można po­peł­nić przez ja­kieś za­szło­ści hi­sto­rycz­ne, usil­ne za­cho­wa­nie kom­pa­ty­bil­no­ści i brak spój­no­ści w de­fi­nio­wa­niu kon­struk­to­rów, nawet tych w Stan­dar­dzie ;)

Pro­ble­my te co jakiś czas są na nowo “od­kry­wa­ne”, ostat­nio po­wró­ci­ły w nieco prze­śmiew­czym ko­dzie na twit­te­rze:

const std::string str = "Modern C++";

std::string s1 { "Modern C++", 3 };
std::string s2 { str, 3 };

std::cout << "S1: " << s1 << "\n";
std::cout << "S2: " << s2 << "\n";

In­tu­icyj­nie mo­gło­by się wy­da­wać, że obiek­ty s1s2 będą za­wie­rać taką samą za­war­tość. Nie­ste­ty to tylko złu­dze­nie.

Wyj­ście z ta­kie­go pro­gra­mu wy­glą­da na­stę­pu­ją­co:

S1: Mod
S2: ern C++

Kod oczy­wi­ście wy­ko­nu­je się po­praw­nie zgod­nie ze stan­dar­dem. Tutaj pro­ble­mem jest mała lo­gicz­na nie­spój­ność w de­fi­ni­cji prze­zna­cze­nia ar­gu­men­tów w kon­struk­to­rach klasy std::string. Z ja­kichś nie­zna­nych po­wo­dów, dla uży­tych w ko­dzie wyżej kon­struk­to­rów, za­leż­nie od typu, okre­ślo­no inne prze­zna­cze­nie ko­lej­no prze­ka­zy­wa­nych ar­gu­men­tów.

Przy two­rze­niu obiek­tu s1 z su­ro­we­go łań­cu­cha zna­ko­we­go ję­zy­ka C - li­te­ra­łu const char*, uży­wa­ny jest kon­struk­tor:

basic_string( const CharT* s,
              size_type count,
              const Allocator& alloc = Allocator() );

który kon­stru­uje obiekt z pierw­szych count zna­ków ciągu wska­zy­wa­ne­go przez s, czyli [0, count].

Na­to­miast w przy­pad­ku s2, łań­cuch ten two­rzo­ny jest z in­ne­go obiek­tu string przez:

basic_string( const basic_string& other,
              size_type pos,
              const Allocator& alloc = Allocator() );

gdzie kon­struk­tor ini­cja­li­zu­je obiekt wszyst­ki­mi zna­ka­mi łań­cu­cha other po­cząw­szy od po­zy­cji pos, czyli [pos, ..].

Mó­wiąc w skró­cie, jeden po­bie­ra 3 pierw­sze znaki z bu­fo­ra, a drugi wszyst­kie od 3 po­zy­cji.

Mimo, że kod dzia­ła po­praw­nie zgod­nie z do­ku­men­ta­cją to można by in­tu­icyj­nie za­ło­żyć, że dla wy­go­dy oba obiek­ty po­win­ny być two­rzo­ne w taki sam spo­sób, a kon­struk­to­ry nie­za­leż­nie od typu po­dą­żać tą samą skład­nią i lo­gi­ką w prze­ka­zy­wa­nych pa­ra­me­trach. Taki wspól­ny in­ter­fejs mógł­by wy­glą­dać jakoś tak:

std::string(const char* s, size_type pos, size_type count = npos);
std::string(const std::string& s, size_type pos, size_type count = npos);
std::string(const std::string_view& s, size_type pos, size_type count = npos);

Takie funk­cje łatwo da­ło­by się uogól­nić do po­sta­ci sza­blo­no­wej:

template<typename T>
std::string(const T&, size_type pos, size_type count = npos);

która o dziwo w po­dob­nej po­sta­ci wy­lą­do­wa­ła w C++17 (w locie z kon­wer­sją przez std::string_view).

Cięż­ko jed­no­znacz­nie stwier­dzić, dla­cze­go w bi­blio­te­ce stan­dar­do­wej ję­zy­ka C++ wy­stę­pu­ją takie nie­spój­no­ści przy two­rze­niu obiek­tów std::string i jakie in­ten­cje przy­świe­ca­ły do­da­niu kon­struk­to­rów o od­mien­nym za­cho­wa­niu.

Jed­nym z pod­wód mogła być opty­ma­li­za­cja/wy­daj­ność. W końcu li­te­ra­ły na­pi­so­we mogą być nie­jaw­nie “kon­wer­to­wa­ne” na strin­gi, a jako “gołe” wskaź­ni­ki łatwo też w nich ma­ni­pu­lo­wać po­zy­cją po­cząt­ko­wą ciągu. Z tego też po­wo­du można było po­mi­nąć taką spe­cja­li­zo­wa­ną me­to­dę two­rze­nia strin­ga z po­da­niem po­zy­cji po­cząt­ko­wej.

Ina­czej wy­glą­da to w przy­pad­ku std::string-a, ale tutaj znów w drugą stro­nę - można już było zre­zy­gno­wać z de­fi­ni­cji de­dy­ko­wa­ne­go kon­struk­to­ra skoro każdy może sobie użyć me­to­dę substr:

std::string str = other.substr(pos, count);

Wtedy nie dzi­wi­ło­by zbyt­nio do­da­nie w C++17 w kla­sie std::string_view po­dob­nej ini­cja­li­za­cji z li­te­ra­łów zna­ko­wych:

constexpr basic_string_view(const CharT* s);
constexpr basic_string_view(const CharT* s, size_type count);

Może dla za­cho­wa­nia spój­no­ści z nie­spój­ny­mi kon­struk­to­ra­mi std::string? :)

Dy­wa­ga­cji i przy­pusz­czeń można snuć wiele. Nie zmie­nia to faktu, że wielu de­we­lo­pe­rów i ko­men­ta­to­rów ze świa­ta C++ uważa klasę std::string za prze­kom­bi­no­wa­ną - po­le­cam lek­tu­rę ar­ty­ku­łu GotW #84: Mo­no­li­ths “Unstrung”. Część metod “ope­ra­cyj­nych” można by­ło­by wy­dzie­lić do zwy­kłych funk­cji po­zo­sta­wia­jąc tylko nie­zbęd­ny in­ter­fejs klasy łań­cu­cho­wej. Takie ze­wnętrz­ne funk­cje, ope­ru­ją­ce także na cią­gach ję­zy­ka C, by­ły­by faj­ny­mi util­sa­mi w std. Ale chyba nie taki kie­ru­nek ob­ra­no w ko­mi­te­cie, bo w ko­lej­nych wy­da­niach, w tym w C++20, do­rzu­co­no kilka no­wych metod do strin­ga.

Trze­ba mieć na uwa­dze to, że strin­gi po­wsta­ły na długo przed STL-​em. A w cza­sie pra­wie 10 let­niej stan­da­ry­za­cji pierw­szej wer­sji ję­zy­ka (‘98) w dużej mie­rze nie pro­jek­to­wa­no od zera nowej bi­blio­te­ki stan­dar­do­wej, a je­dy­nie zin­te­gro­wa­no wiele ist­nie­ją­cych już klas1 i do­pa­so­wa­no do mo­de­lu STL-a. W wielu przy­pad­kach za­cho­wu­jąc wstecz­ną zgod­ność.

O za­cho­wa­niu wstecz­nej kom­pa­ty­bil­no­ści na po­zio­mie kodu i ABI w C++ można też by­ło­by długo mówić…


BTW. Od dawna na mojej li­ście go­ścił temat nie­spój­no­ści i dziw­nych kon­struk­cji bi­blio­te­ki stan­dar­do­wej. Po­wo­li z każ­dym po­stem i ar­ty­ku­łem w sieci, na jakie mi­mo­wol­nie na­tra­fia­łem w co­dzien­nym sur­fo­wa­niu, od­świe­ża­łem ten temat. Choć wspo­mnia­ny w ar­ty­ku­le szum na twit­te­rze przy­po­mniał mi o tym cie­ka­wym zna­le­zi­sku…

To jed­nak do­pie­ro wy­pusz­cze­nie przez Ja­so­na Tur­ne­ra ostat­nie­go epi­zo­du C++ We­ekly po­ru­sza­ją­ce­go do­kład­nie ten pro­blem zmu­si­ło mnie do za­koń­cze­nia edy­cji i pu­bli­ka­cji tego mo­je­go dra­ftu ;)


Przy­pi­sy

  1. Wspo­mi­nał o tym Ni­co­lai M. Jo­sut­tis w swo­jej książ­ce “C++. Bi­blio­te­ka stan­dar­do­wa. Pod­ręcz­nik pro­gra­mi­sty” (ory­gi­nal­ne wy­da­nie: The C++ Stan­dard Li­bra­ry: A Tu­to­rial and Re­fe­ren­cee). ↩︎

Ko­men­ta­rze (0)

Dodaj ko­men­tarz

/do­zwo­lo­ny mark­down/

/nie zo­sta­nie opu­bli­ko­wa­ny/