static_cast unsigned -> signed char

Fórumok

Sziasztok,

van egy feladat, mondjuk egy

std::string

típusú változóban kell a szóközöket (incl. tabulátorok...) kipucolni, erre volt egy ilyen kód:


========
std::string removewsp(std::string value) {

    int64_t i = 0;

    while (i < value.size()) {
        if (isspace(value[i])) {
            value.erase(i, 1);
        } else {
          i++;
        }
    }

    return value;
}
========

Aztán kitalálták, hogy az isspace() nem elég, mert pl az ASCII 160-at (NBSP) nem törli, így kegészítették erre:


========
#define NBSP 160

std::string removewsp(std::string value) {

    int64_t i = 0;

    while (i < value.size()) {
        if (isspace(value[i]) || (value[i] == NBSP)) {
            value.erase(i, 1);
        } else {
          i++;
        }
    }

    return value;
}
========

Ez viszont nem jó, mert egyrészt szól a fordító, hogy az ott soha nem lesz igaz, másrészt a fordítónak igaza van. (Úgy tudom az std::string mint típus char szekvencia, a "sima" char pedig signed, így az nem nagyon lesz soha 128-nál nagyobb értékű, de lehet -127 is).

Volt egy javaslatom, hogy akkor csináljuk így:


========
        if (isspace(value[i]) || (value[i] == static_cast<char>(NBSP))) {
========

mert szerintem ha egy olyan unsigned char tömbből csinálunk std:string-et, aminek az egyik eleme nagyobb, mint 128, akkor a fordító szépen elvégzi a castolást, így a 160-ból pl -96 lesz.


========
    unsigned char c_array = {102, 111, 111, 160, 98, 97, 114}; // "foo\0xA0bar"
    std::string c_str(c_array);
    value = c_str;
========

Ha ezt megnézem karakterenként, akkor a 3. helyen (0-tól) -96 lesz. Ráadásul a függvény is jól működik, míg cast nélkül a 160-at (értelemszerűen) nem találja meg.

A válasz az ötletre az volt, hogy ez csak a warning-ot szünteti meg, nem megoldás a problémára.

Szeretek tanulni és megérteni dolgokat, és mivel egyéb válasz nem jött, így kérdezem a T. Nagyérdeműt: miért csak a warning-ot szünteti meg a static_cast, és mi lenne a helyes megoldás?

Köszi.

Hozzászólások

> Ráadásul a függvény is jól működik [...] ez csak a warning-ot szünteti meg, nem megoldás a problémára

Most akkor működik, vagy sem? Mert az idézett két rész ellentmond egymásnak.

Off: 2019-ben már nagyon nem ildomos latin-1/2 kódkészlettel dolgozni.

Őőő... bocs, melyik két része mond ellent egymásnak? :)

A fv működik (szerintem), ott a példa is, akit érdekel, ki tudja próbálni.

Az ok, amire hivatkoztak, hogy nem jó, mert "csak a warning-ot rejti el a fordítás során". Szerintem nem. Szerintem egyrészt az elvi megoldás is jó és a magyarázat, ill a gyakorlati is (hiszen működik a fv).

Erre vagyok kíváncsi: én tévedek valamiben, és igaza volt annak, aki a ezt az okot mondta, vagy ő tévedett.

A régi kód nem működött.

Mit értesz régi kód alatt?

Az új működik, vagy nem?

Mit értesz új alatt? Amit én javasoltam, de végül nem az lett?

Ha működik, akkor nyilván nem csak a warningot rejtette el a változtatás, nemdebár?

De igen, de ez maga a kérdés: tényleg csak a warningot rejti el, és nem oldja meg a problémát, vagy nem csak elrejt, és megoldja?

A második és harmadik verziódat értettem.

A második verzióra ezt írtad: "Ez viszont nem jó". A harmadikra pedig ezt: "Ráadásul a függvény is jól működik".

> De igen, de ez maga a kérdés: tényleg csak a warningot rejti el, és nem oldja meg a problémát, vagy nem csak elrejt, és megoldja?

Kipróbáltad a kódot? Jól futott? Akár igen, akár nem, a kérdésedet ez meg kell hogy válaszolja (az egyik esetben az egyik, a másik esetben a másik irányba). Tényleg ne haragudj, nem értem, mi hiányzik részedről annak a megválaszolásához, hogy megjavult-e a kód, vagy csak a warning tűnt el.

A második verzióra ezt írtad: "Ez viszont nem jó".
Igen, a második van most a kódban, és egyrészt szól miatta a fordító, másrészt szerintem tényleg nem jó.

A harmadikra pedig ezt: "Ráadásul a függvény is jól működik".
Igen, a harmadik az, amit én javasoltam. Erre nem panaszkodik a fordító, és csak "labor" körülmények között tudtam kipróbálni (mivel nem találtam olyan helyzetet, hogy C++ std::string egyik karaktere [-127;128] intervallumon kívül essen), de amúgy jó. És van egy elég komoly regressziós teszt sor is, ami alapján a korábbi tesztek - természetesen jók.

Kipróbáltad a kódot? Jól futott? Akár igen, akár nem, a kérdésedet ez meg kell hogy válaszolja

Igen, csak én ilyen komplexusos alkat vagyok :), ha azt mondják, hogy nem jó, akkor megpróbálok utánanézni, miért nem jó :).

> És van egy elég komoly regressziós teszt sor is, ami alapján a korábbi tesztek - természetesen jók.

És remélem új teszt is, amely a régi kóddal elhasal, az újjal lefut helyesen.

> ha azt mondják, hogy nem jó, akkor megpróbálok utánanézni, miért nem jó

SZVSZ ha szerinted viszont jó, és ezt alátámasztja a viselkedés amit tapasztalsz, érdemes első körben annál az embernél rákérdezni, aki szerint nem. :) A reviewer is csak ember, tévedhet ő is.

Esetleg:


if (isspace(value[i]) || value[i] == (char)NBSP)

Avagy:


static const char NBSP= '\xa0';

Egyébként a 'char' az tényleg a 'signed char' és az 'unsigned char' valamelyike, de mivel platformfüggő, nem lehet előre tudni, melyik. (Vö: mit tett értünk Nyomasek Bobó.)

Ez volt az első ötlet, de mivel ez C++, gondoltam éljünk a lehetőségekkel.

Utánanéztem, mi a különbség, és annyit találtam, hogy a static_cast fordításkor ellenőrzi, hogy lehetséges-e a cast maga, míg a C stílusú cast esetén ez majd futáskor fog eldölni.... :)

A makro helyett változó szerintem elméletben ugyanúgy cast.

signed/unsigned - és tényleg:
https://en.cppreference.com/w/cpp/language/types#Character_types

Akkor érdemes megvizsgálni valami feltétellel, milyen értékei lehetnek, és attól függően cast-olni?

ps: amúgy valszeg szar a példa, amiben unsigned char-ból akarok std::string-et csinálni, mert egyik fordító sem fogadja el. Így viszont nem nagyon értem, hogy (adott plattformon) hogy lehet std::string-ben pl 160-as érték...

> Akkor érdemes megvizsgálni valami feltétellel, milyen értékei lehetnek, és attól függően cast-olni?

Nem érdemes. Csak írd ki a literál elé, hogy (char), mint pl (char)160

> amúgy valszeg szar a példa, amiben unsigned char-ból akarok std::string-et csinálni, mert egyik fordító sem fogadja el. Így viszont nem nagyon értem, hogy (adott plattformon) hogy lehet std::string-ben pl 160-as érték...

Nyolcbites kontextusban a 160 lényegében ugyanazt jelenti, mint a -96 vagy a '\xa0', nem kell túl sokat gondolni bele.

"Nyolcbites kontextusban a 160 lényegében ugyanazt jelenti, mint a -96 vagy a '\xa0', nem kell túl sokat gondolni bele."
Valóban ugyanaz a bitminta, de a relációs operátoroknál nem mindegy, hogy 5 > 160 igaz-e.
Azaz nem mindegy, hogy azt a nyolc bites bitmintát hogyan értelmezed.

ASCII 160 Ilyen nincs, mert az ASCII az 7 bites, nem 8.

Szóval szerintem célszerű lenne a char-t a tényleges értékével, vagyis -96-al összehasonlítani, és nem szórakozni unsigned char-al (ami senki sem használ).

Plusz használj for-t és egy temp stringet, mert ez így mocsok lassú lesz.

ASCII 160 Ilyen nincs, mert az ASCII az 7 bites, nem 8.

Igen, már én is itt akadok el, ill azon, hogy egy sdt::string-be hogy lehet 160-as karaktert tenni?

Plusz használj for-t és egy temp stringet, mert ez így mocsok lassú lesz.
Köszi - esetleg erről van vmi doksi, hogy miért?

> Helyben nagyon csunya lenne

Miért?

> egyenkent masolgatni char-okat lassu.

Nyilván gyorsabb 4 vagy 8 byte-os egységeket másolni, legalábbis amíg a forrás és a cél azonos módon van „align”-olva a memóriában. Ha nem, akkor fogalmam sincs, hogy hogy a leggyorsabb, bízzuk ezt rá azokra akik implementálták, hogy alaposan utánajártak.

A value.size() - forditotol fuggoen - vegigmegy minden egyes iteracional a stringeden. Szoval ha n a hossza, akkor n*(n-1) lepessel - ha nem torolsz sokat.

Az erase-t hogy implementalnad egy string eseten? Gondolom a torleshez minden egyes utana kovetkezo karaktert arrebbmasolsz, nem? Megint nem a leghatekonyabb megoldas, ha tele van torlendo karakterrel..

Ezzel szemben ha egyszer lefoglalsz egy temp. stringet, es ebbe masolod, akkor a value.size()-ot eleg, ha egyszer kiertekeled (utana beteheted egy valtozoba), es a koltseges torlest is megsporolod.

--
When you tear out a man's tongue, you are not proving him a liar, you're only telling the world that you fear what he might say. -George R.R. Martin

Hehe.
Volt az IOCCC, Underhanded C Code Contest, meg hasonlok mintajara egy verseny C++-szal is. A feladat az volt, hogy egy artatlanul kinezo kod ne teljesen azt csinalja (vagy ne annyi ido alatt) ket kulonbozo forditoval. Az a versenyzo nyerte, aki tudta, hogy a - talan - Visual C++ es GCC size implementacioja elter, az egyik mindig ujraszamolja, a masik meg nem. Igy a kodja egy nagyon hosszu stringet megadva nagyon lassan futott az ujraszamoloson.

(ez persze tervezoi dontes, ha a hosszt nyilvantartod, annak is lehet hatranya.. ha nem tartod nyilvan, de a kodban tudod, hogy ciklusban nem valtozik, es megjegyzed, akkor megint mindegy)

--
When you tear out a man's tongue, you are not proving him a liar, you're only telling the world that you fear what he might say. -George R.R. Martin

Én tuti, hogy (C-ben) temp stringbe pakolnám azokat a karaktereket, melyek megfelelnek a kritériumnak, közben két számlálót használnék a léptetésre.

De ezért írtam a kérdést, mert nem tudom, hogy az erase() hogy működik - én ilyenkor mindig arra gondolok, hogy nálam sokkal okosabbak tervezték és implementálták az adott algoritmust, és tuti, hogy gyorsabb és jobb, mint amit én írnék :).

> Én tuti, hogy (C-ben) temp stringbe pakolnám azokat a karaktereket, melyek megfelelnek a kritériumnak, közben két számlálót használnék a léptetésre.

Ez jó hozzáállás.

> nem tudom, hogy az erase() hogy működik

Teljesen mindegy, hogy hogyan működik – ha többször hívod meg egy-egy karakter törlésére (márpedig csak így lehet), már garantáltan algoritmikusan rosszabb, mint ha egy menetben ugranál át minden érdektelen karaktert a másolás során. Tehát en bloc rossz választás az erase() erre a feladatra – már feltéve, hogy ez számít egyáltalán. Mert például ha garantált, hogy a bemeneti sztring legfeljebb 20-30 karakter, talán jó ha 4-5 szóközzel, akkor baromira tök mindegy.

A másik sztringbe átpakolás, egyszeri végigfutással a sztringen jobb, mint több másik korábban látott megközelítés. Ugyanezt helyben csinálva még jobb.

> a végén egy 'resize(újhossz)' kell majd (ha volt egyáltalán törölt karalter)

Egyszerűbb, átláthatóbb, olcsóbb, gyorsabb stb. egy 'resize(újhossz)' feltétel nélkül, mintsem feltételesen.

Ha a char típus történetesen unsigned, akkor simán. Ha signed, akkor -96 lesz belőle. Amúgy a (char)160 teljesen jó megoldás, nem csak a warningot tünteti el. a static_cast pedig tökugyanazt csinálja ebben az esetben, mint a (char) cast (nincs ilyen run-time/compile-time különbség köztük).

És egy apró megjegyzés: a char, signed char, unsigned char 3 különböző típus. Tehát, ha pl. a char előjeles, akkor se ugyanaz a típus, mint a signed char. Ugyanazokat az értékeket tudja eltárolni, de külön típusok. Pl. tudsz külön overloadot csinálni mind3 char típusra.

Húha, bonyolult kérdés ez.
Egyrészt, az ASCII az egy 7 bites kódolás, 127 felett nincs is egyáltalán code point.
A 160 mint NBSP az ISO 8859-1 kódolás szerint igaz. Ez már valóban 8 bites kódolás, de ez nem ASCII.
Egyáltalán definiálva van, hogy a bemenet az ISO 8859-1 kódolású?
Az std::string olyan karakterkészletekre jó, amiknél egy karakter egy byte-ra kódolódik. Ebből azért sokféle van.
A szerencséd az, hogy az ISO 8859 bármelyik alfajában a 160-as karakter az NBSP.

Igazi emberi szöveg kezelésére az std::string nem jó, erre vannak sokkal jobb megoldások, pl. az IBM ICU.
http://site.icu-project.org/
Ez korrektul képes arra, hogy egy bytesorozatot megadott karakterkódolással az adott kódolásnak megfelelő karakterkészlet karakterláncaként tudja értelmezni, le tudod kérdezni, hogy az adott karakter az whitespace-e. És arra is többféle definíció van még Unicode-ban is, hogy mi a whitespace.
Lásd: https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uchar_8h.h…

Egyáltalán definiálva van, hogy a bemenet az ISO 8859-1 kódolású?

Hát nem igazán :)
https://www.ietf.org/rfc/rfc2616.txt

Az std::string olyan karakterkészletekre jó, amiknél egy karakter egy byte-ra kódolódik.
Itt az első dilemma: az std::string bár byte-ot tárol, de az értéke -127 és 128 között lehet. Viszont az inputról nem jöhet 0-nál kisebb érték, max ha a kódban valahol cast-olódik. Ha viszont ez történik, akkor az összehasonlítandó értéknek is -127 és 128 között kell lennie, tehát (szerintem) jónak kellene lennie a cast-nak.

"az std::string bár byte-ot tárol"
Nem igaz. Az std::string char-okat tárol. A char pedig definíció szerint az adott architektúrán a legjobbab használható a signed char és unsigned char közül.
"char - type for character representation which can be most efficiently processed on the target system (has the same representation and alignment as either signed char or unsigned char, but is always a distinct type)."

"Viszont az inputról nem jöhet 0-nál kisebb érték"
Ez a kódból nem derül ki.

> "az std::string bár byte-ot tárol"
Nem igaz. Az std::string char-okat tárol.

Te írtad előtte, hogy Az std::string olyan karakterkészletekre jó, amiknél egy karakter egy byte-ra kódolódik :)

Én feljebb írtam már, hogy az std::string char - és hogy signed/unsigned, azt is megtaláltam már a ref-ben. Amúgy a char attól függetlenül hogy signed/unsigned, 1 byte-on tárolódik, különben az unsigned 160 nem lehetne signed -96. :)

Jajjj, na ezért használok mindenre Java-t meg UTF-8-at, nincs időm ilyenekkel bajlódni, gizike gépében meg van elég RAM

Az hogy belül mi van, komolyan hol fog előkerülni? Nem fogok tömbökkel meg malloc-kal játszani, és mondom, hogy gizikének van elég RAM-ja, úgyhogy az se zavar, ha 64biten tárol egy karaktert, amíg jól dolgozik és használható marad és be tudok olvasni és ki tudok írni UTF-8 kódolás doksit, egyedül a hülye BOM zavar be, amit a picipuhán kívül más nem használ.

Na, jó tudni :) Gugliztam ennek hatására, és most ezt találtam:
https://www.baeldung.com/java-9-compact-string

Főleg ez tetszik, ami amúgy el is várható:

"Note that all the changes made for Compact String, are in the internal implementation of the String class and are fully transparent for developers using String."

Két szemléletes példát írok feldolgozásra:

1.) C-ben tördelj fel egy char *-ot szóközök mentén. Az idézőjel, azaz nem a " (0x22) ASCII macskaköröm, hanem „ (0x201e) és ” (0x201d) jelek és sok más karakter fankón eltűnik egy sima C bytefeldolgozás esetén.

2.) C-ben tördelj például 20 széles kijelző dobozra szét szöveget (nem kell elválasztás, csak belepaszirozó tördelés). Ha 20 byte-onként beszúrsz \n-t, lesznek rövidebb soraid a több byte-os UTF8 karaktereid miatt és lesznek értelmezhetetlen sorvégi és értelmezhetetlen soreleji karaktereid.

Hali,

a #define használata nem túl szerencsés, és a cast sem túl szép, ahogy többen írták, célszerűbb egy megfelelő típusú konstanst használni, pl így:


std::string removewsp(std::string value) {
  constexpr char NBSP = 160;
  auto it = std::remove_if(value.begin(), value.end(), [](char c) {
    return std::isspace(c) || c == NBSP;
  });
  value.resize(it - value.begin());
  return value;
}

vagy így:


std::string removewsp2(const std::string& value) {
  constexpr char NBSP = 160;
  std::string res;
  res.reserve(value.size());
  std::copy_if(value.begin(), value.end(), std::back_inserter(res), [](char c) {
    return !(std::isspace(c) || c == NBSP);
  });
  return res;
}

Mindkét fenti példa lineáris futásidejű. Nekem a remove_if verzió kicsit szimpatikusabb, mert jobban kifejezi, hogy mit is akarunk csinálni, illetve könnyebben átalakítható olyan verzióra ami "helyben" szűri meg (alakítja át) a string-et.

Köszi a tippeket.

Egy megjegyzés:


    constexpr char NBSP = 160;

Erre a fordító ugyanúgy warningol (de legalább helyesn működik az algorutmus :))


warning: implicit conversion from 'int' to 'const char' changes value from 160 to -96

Amit a cast megold ((cast)160), de akkor meg ugyanott vagyok, mint a static_cast()-nál, vagy az összehasonlításnál beállított cast-nál. És a kérdés igazából erre vonatkozott, hogy egy változót, aminek 160 (0xA0) az értéke hogy tudom összehasonlítani egy std::string szekvencia egy elemével, ami -128 és 127 között lehet, ill. jó-e erre (akármelyik) cast.

Amúgy maga az algoritmus szép, köszi mégegyszer. :)