Was ist die Idee hinter ^ = 32, die Kleinbuchstaben in Großbuchstaben umwandelt und umgekehrt?

146

Ich habe ein Problem mit Codeforces gelöst. Normalerweise überprüfe ich zuerst, ob das Zeichen ein oberer oder unterer englischer Buchstabe ist, subtrahiere oder addiere es 32, um es in den entsprechenden Buchstaben umzuwandeln. Aber ich habe jemanden gefunden, ^= 32der das Gleiche tut. Hier ist es:

char foo = 'a';
foo ^= 32;
char bar = 'A';
bar ^= 32;
cout << foo << ' ' << bar << '\n'; // foo is A, and bar is a

Ich habe nach einer Erklärung dafür gesucht und es nicht herausgefunden. Warum funktioniert das so?

Devon
quelle
5
en.wikipedia.org/wiki/File:USASCII_code_chart.png Tipp: Sie können @mithilfe von in ` konvertieren ^ 32.
KamilCuk
112
FWIW, es "funktioniert" nicht wirklich. Es funktioniert für diesen bestimmten Zeichensatz, aber es gibt andere Sätze, bei denen dies nicht der Fall ist. Sie sollten die Groß- toupperund tolowerKleinschreibung verwenden und umschalten.
NathanOliver
7
irgendwann bei Online-Wettbewerben "ist die Idee", Code so verschleiert zu schreiben, dass er niemals eine ernsthafte Überprüfung bestehen würde;)
idclev 463035818
21
^ = transformiert den Wert mit XOR. ASCII-Großbuchstaben haben im entsprechenden Bit eine Null, Kleinbuchstaben eine Eins. Das heißt, bitte nicht! Verwenden Sie geeignete Zeichenroutinen (Unicode), um zwischen Klein- und Großbuchstaben zu konvertieren. Die Ära von ASCII ist längst vorbei.
Hans-Martin Mosner
14
Es ist nicht nur so, dass es nur mit einigen Zeichensätzen funktioniert. Selbst wenn wir annehmen, dass die ganze Welt UTF-8 ist (was zumindest ein schönes utopisches Ziel sein könnte), funktioniert es auch nur mit den 26 Buchstaben Aan Z. Das ist in Ordnung, solange Sie sich nur für Englisch interessieren (und keine Schreibweisen "naiv", Wörter wie "Café" oder Namen mit diakritischen Zeichen verwenden ...), aber die Welt ist nicht nur Englisch.
Ilkkachu

Antworten:

149

Werfen wir einen Blick auf die ASCII-Codetabelle in Binärform.

A 1000001    a 1100001
B 1000010    b 1100010
C 1000011    c 1100011
...
Z 1011010    z 1111010

Und 32 ist 0100000der einzige Unterschied zwischen Klein- und Großbuchstaben. Wenn Sie dieses Bit umschalten, wird der Fall eines Buchstabens umgeschaltet.

Hanjoung Lee
quelle
49
"schaltet den Fall um" * nur für ASCII
Mooing Duck
39
@Mooing nur für A-Za-z in ASCII. Kleinbuchstaben von "[" sind nicht "{".
dbkk
21
@dbkk {ist kürzer als [, es ist also ein "Kleinbuchstabe". Nein? Ok, ich werde mich zeigen: D
Peter Badida
25
Trivia Leckerbissen: Im 7 - Bit - Bereich, Deutsch Computer hatte [] {|} zu ÄÖÜäöü neu zugeordnet , da wir Umlaute benötigt mehr als die Charaktere, so in diesem Zusammenhang {(ä) tatsächlich war die Klein [(Ä).
Guntram Blohm unterstützt Monica
14
@GuntramBlohm Weitere Kleinigkeiten, aus diesem Grund betrachten IRC-Server identische Spitznamen foobar[]und gelten foobar{}als identische Spitznamen, da bei Spitznamen die Groß- und Kleinschreibung nicht berücksichtigt wird und IRC seinen Ursprung in Skandinavien hat :)
ZeroKnight
117

Dies nutzt die Tatsache, dass ASCII-Werte von wirklich klugen Leuten gewählt wurden.

foo ^= 32;

Dadurch wird das sechstniedrigste Bit 1 von foo(das Großbuchstaben-Flag von ASCII) umgedreht, wodurch ein ASCII-Großbuchstabe in einen Kleinbuchstaben umgewandelt wird und umgekehrt .

+---+------------+------------+
|   | Upper case | Lower case |  32 is 00100000
+---+------------+------------+
| A | 01000001   | 01100001   |
| B | 01000010   | 01100010   |
|            ...              |
| Z | 01011010   | 01111010   |
+---+------------+------------+

Beispiel

'A' ^ 32

    01000001 'A'
XOR 00100000 32
------------
    01100001 'a'

Und durch Eigentum von XOR , 'a' ^ 32 == 'A'.

Beachten

C ++ ist nicht erforderlich, um ASCII zur Darstellung von Zeichen zu verwenden. Eine andere Variante ist EBCDIC . Dieser Trick funktioniert nur auf ASCII-Plattformen. Eine tragbarere Lösung wäre die Verwendung von std::tolowerund std::touppermit dem angebotenen Bonus, um das Gebietsschema zu kennen (es löst jedoch nicht automatisch alle Ihre Probleme, siehe Kommentare):

bool case_incensitive_equal(char lhs, char rhs)
{
    return std::tolower(lhs, std::locale{}) == std::tolower(rhs, std::locale{}); // std::locale{} optional, enable locale-awarness
}

assert(case_incensitive_equal('A', 'a'));

1) Da 32 1 << 5(2 hoch 5) ist, wird das 6. Bit umgedreht (von 1 gezählt).

YSC
quelle
16
EBCDIC wurde auch von einigen sehr klugen Leuten ausgewählt: funktioniert sehr gut mit Lochkarten, vgl. ASCII, das ist ein Chaos. Aber das ist eine schöne Antwort, +1.
Bathseba
65
Ich weiß nichts über Lochkarten, aber ASCII wurde auf Papierband verwendet. Aus diesem Grund wird das Zeichen "Löschen" als 1111111 codiert: Sie können also jedes Zeichen als "gelöscht" markieren, indem Sie alle Löcher in seiner Spalte auf dem Band ausstanzen.
Dan04
23
@Bathsheba als jemand, der keine Lochkarte benutzt hat, ist es sehr schwierig, mich mit der Idee zu beschäftigen, dass EBCDIC intelligent gestaltet wurde.
Lord Farquaad
9
@LordFarquaad IMHO ist das Wikipedia-Bild, wie Buchstaben auf eine Lochkarte geschrieben werden, ein offensichtliches Beispiel dafür, wie EBCDIC für diese Codierung einen gewissen (aber nicht vollständigen, siehe / vs S) Sinn ergibt. en.wikipedia.org/wiki/EBCDIC#/media/…
Peteris
11
@ dan04 Hinweis zu erwähnen "Was ist die Kleinbuchstabenform von 'MASSE'?". Für diejenigen, die es nicht wissen, gibt es zwei deutsche Wörter, deren Großbuchstabenform MASSE ist. einer ist "Masse" und der andere ist "Messungen". Richtig tolowerauf Deutsch braucht nicht nur ein Wörterbuch, es muss in der Lage sein, die Bedeutung zu analysieren.
Martin Bonner unterstützt Monica
35

Lassen Sie mich sagen, dass dies - obwohl es klug erscheint - ein wirklich, wirklich dummer Hack ist. Wenn Ihnen dies 2019 jemand empfiehlt, schlagen Sie ihn. Schlagen Sie ihn so hart wie möglich.
Sie können dies natürlich in Ihrer eigenen Software tun, die Sie und niemand anderes verwendet, wenn Sie wissen, dass Sie sowieso nie eine andere Sprache als Englisch verwenden werden. Ansonsten nicht gehen.

Der Hack war vor 30-35 Jahren wohl "OK", als Computer nicht wirklich viel konnten, sondern Englisch in ASCII und vielleicht eine oder zwei wichtige europäische Sprachen. Aber ... nicht mehr so.

Der Hack funktioniert, weil US-lateinische Groß- und Kleinschreibung genau 0x20voneinander getrennt sind und in derselben Reihenfolge angezeigt werden, was nur ein kleiner Unterschied ist. Was in der Tat dieses bisschen Hack umschaltet.

Jetzt waren die Leute, die Codepages für Westeuropa und später das Unicode-Konsortium erstellten, klug genug, um dieses Schema für z. B. deutsche Umlaute und Vokale mit französischem Akzent beizubehalten. Nicht so für ß, die (bis jemand das Unicode-Konsortium 2017 überzeugte und ein großes Fake News-Printmagazin darüber schrieb und den Duden tatsächlich überzeugte - kein Kommentar dazu) nicht einmal als Versal existieren (verwandelt sich in SS) . Jetzt tut es existieren als versal, aber die beiden sind 0x1DBFPositionen auseinander, nicht 0x20.

Die Implementierer waren jedoch nicht rücksichtsvoll genug, um dies aufrechtzuerhalten. Wenn Sie beispielsweise Ihren Hack in einigen osteuropäischen Sprachen oder ähnlichem anwenden (ich würde nichts über Kyrillisch wissen), werden Sie eine böse Überraschung erleben. Alle diese "Beil" -Zeichen sind Beispiele dafür, Klein- und Großbuchstaben sind voneinander getrennt. Der Hack tut es also funktioniert dort nicht richtig.

Es gibt noch viel mehr zu beachten. Einige Zeichen werden beispielsweise nicht einfach von Klein- in Großbuchstaben umgewandelt (sie werden durch unterschiedliche Sequenzen ersetzt), oder sie können ihre Form ändern (was unterschiedliche Codepunkte erfordert).

Denken Sie nicht einmal darüber nach, was dieser Hack für Dinge wie Thai oder Chinesisch tun wird (es gibt Ihnen nur völligen Unsinn).

Das Speichern von ein paar hundert CPU-Zyklen hat sich vor 30 Jahren vielleicht sehr gelohnt, aber heutzutage gibt es wirklich keine Entschuldigung dafür, einen String richtig zu konvertieren. Es gibt Bibliotheksfunktionen zum Ausführen dieser nicht trivialen Aufgabe.
Die Zeit, die benötigt wird, um mehrere Dutzend Kilobyte Text richtig zu konvertieren, ist heutzutage vernachlässigbar.

Damon
quelle
2
Ich stimme voll und ganz zu - obwohl es für jeden Programmierer eine gute Idee ist zu wissen, warum es funktioniert - könnte sogar eine gute Interviewfrage stellen. Was macht das und wann sollte es verwendet werden :)
Bill K
33

Es funktioniert, weil der Unterschied zwischen 'a' und A 'in ASCII und abgeleiteten Codierungen zufällig 32 beträgt und 32 auch der Wert des sechsten Bits ist. Das Umdrehen des 6. Bits mit einem exklusiven ODER konvertiert somit zwischen oberem und unterem.

Jack Aidley
quelle
22

Höchstwahrscheinlich wird Ihre Implementierung des Zeichensatzes ASCII sein. Wenn wir uns die Tabelle ansehen:

Geben Sie hier die Bildbeschreibung ein

Wir sehen, dass es einen Unterschied 32zwischen dem Wert einer Klein- und Großbuchstabenzahl gibt. Wenn wir dies tun ^= 32(was dem Umschalten des 6. niedrigstwertigen Bits entspricht), ändert sich daher zwischen einem Klein- und einem Großbuchstaben.

Beachten Sie, dass es mit allen Symbolen funktioniert, nicht nur mit den Buchstaben. Es schaltet ein Zeichen mit dem jeweiligen Zeichen um, wobei das 6. Bit unterschiedlich ist, was zu einem Zeichenpaar führt, das zwischen den Zeichen hin- und hergeschaltet wird. Für die Buchstaben bilden die jeweiligen Groß- / Kleinbuchstaben ein solches Paar. A NULwird sich in Spaceund umgekehrt ändern und @mit dem Backtick umschalten. Grundsätzlich wechselt jedes Zeichen in der ersten Spalte dieses Diagramms mit dem Zeichen eine Spalte weiter, und das Gleiche gilt für die dritte und vierte Spalte.

Ich würde diesen Hack jedoch nicht verwenden, da es keine Garantie dafür gibt, dass er auf jedem System funktioniert. Verwenden Sie stattdessen einfach toupper und tolower und Fragen wie isupper .

Blaze
quelle
2
Nun, es funktioniert nicht für alle Buchstaben mit einem Unterschied von 32. Andernfalls würde es zwischen '@' und '' funktionieren!
Matthieu Brucher
2
@MatthieuBrucher Es funktioniert, 32 ^ 32ist 0, nicht 64
NathanOliver
5
'@' und '' sind keine "Buchstaben". Nur [a-z]und [A-Z]sind "Buchstaben". Der Rest sind Zufälle, die der gleichen Regel folgen. Wenn dich jemand nach "Großbuchstaben]" fragen würde, was wäre das? es wäre immer noch "]" - "}" ist nicht der "Großbuchstabe" von "]".
Freiheit-m
4
@MatthieuBrucher: Eine andere Möglichkeit, diesen Punkt zu verdeutlichen, besteht darin, dass die alphabetischen Bereiche in Klein- und Großbuchstaben %32im ASCII-Codierungssystem keine "Alignment" -Grenze überschreiten . Aus diesem Grund ist Bit 0x20der einzige Unterschied zwischen den Groß- / Kleinbuchstaben desselben Buchstabens. Wenn dies nicht der Fall wäre, müssten Sie addieren oder subtrahieren 0x20, nicht nur umschalten, und für einige Buchstaben würde es eine Ausführung geben, um andere höhere Bits umzudrehen. (Und die gleiche Operation konnte nicht umschalten, und es wäre schwieriger, zunächst nach alphabetischen Zeichen |= 0x20zu suchen, da man lcase nicht erzwingen konnte.)
Peter Cordes
2
+1, um mich an all diese Besuche bei asciitable.com zu erinnern, um genau diese Grafik (und die erweiterte ASCII-Version !!) der letzten 15 oder 20 Jahre zu betrachten?
AC
15

Hier gibt es viele gute Antworten, die beschreiben, wie dies funktioniert, aber warum es so funktioniert, ist die Verbesserung der Leistung. Bitweise Operationen sind schneller als die meisten anderen Operationen innerhalb eines Prozessors. Sie können schnell einen Vergleich ohne Berücksichtigung der Groß- und Kleinschreibung durchführen, indem Sie einfach nicht auf das Bit schauen, das die Groß- und Kleinschreibung bestimmt, oder die Groß- / Kleinschreibung einfach ändern, indem Sie das Bit umdrehen (die Leute, die die ASCII-Tabelle entworfen haben, waren ziemlich schlau).

Offensichtlich ist dies aufgrund schnellerer Prozessoren und Unicode nicht annähernd so wichtig wie 1960 (als die Arbeit an ASCII begann), aber es gibt immer noch einige kostengünstige Prozessoren, die einen signifikanten Unterschied bewirken könnten solange Sie nur ASCII-Zeichen garantieren können.

https://en.wikipedia.org/wiki/Bitwise_operation

Bei einfachen kostengünstigen Prozessoren sind bitweise Operationen in der Regel wesentlich schneller als die Division, um ein Vielfaches schneller als die Multiplikation und manchmal erheblich schneller als die Addition.

HINWEIS: Ich würde aus verschiedenen Gründen (Lesbarkeit, Korrektheit, Portabilität usw.) die Verwendung von Standardbibliotheken für die Arbeit mit Zeichenfolgen empfehlen. Verwenden Sie Bit Flipping nur, wenn Sie die Leistung gemessen haben und dies Ihr Engpass ist.

Brian
quelle
14

So funktioniert ASCII, das ist alles.

Wenn Sie dies ausnutzen, geben Sie die Portabilität auf, da C ++ nicht auf ASCII als Codierung besteht.

Aus diesem Grund sind die Funktionen std::toupperund std::tolowerin der C ++ - Standardbibliothek implementiert - Sie sollten diese stattdessen verwenden.

Bathseba
quelle
6
Es gibt jedoch Protokolle, für die die Verwendung von ASCII erforderlich ist, z. B. DNS. Tatsächlich wird der "0x20-Trick" von einigen DNS-Servern verwendet, um zusätzliche Entropie als Anti-Spoofing-Mechanismus in eine DNS-Abfrage einzufügen. DNS unterscheidet nicht zwischen Groß- und Kleinschreibung, soll aber auch zwischen Groß- und Kleinschreibung schützen. Wenn Sie also eine Abfrage mit zufälliger Groß- und Kleinschreibung senden und dieselbe Groß- / Kleinschreibung zurückerhalten, ist dies ein guter Hinweis darauf, dass die Antwort nicht von einem Dritten gefälscht wurde.
Alnitak
Es ist erwähnenswert, dass viele Codierungen immer noch dieselbe Darstellung für die Standard-ASCII-Zeichen (nicht erweitert) haben. Wenn Sie sich jedoch wirklich Sorgen über unterschiedliche Codierungen machen, sollten Sie die richtigen Funktionen verwenden.
Captain Man
5
@ CaptainMan: Auf jeden Fall. UTF-8 ist eine Sache von purer Schönheit. Hoffentlich wird es in den C ++ - Standard "aufgenommen", sofern IEEE754 Gleitkommawerte hat.
Bathseba
11

Siehe die zweite Tabelle unter http://www.catb.org/esr/faqs/things-every-hacker-once-knew/#_ascii und die folgenden Hinweise, die unten wiedergegeben sind:

Der Control-Modifikator auf Ihrer Tastatur löscht im Wesentlichen die oberen drei Bits des von Ihnen eingegebenen Zeichens, belässt die unteren fünf und ordnet sie dem Bereich 0..31 zu. So bedeuten beispielsweise Strg-LEERTASTE, Strg-@ und Strg-`alle dasselbe: NUL.

Sehr alte Tastaturen verwendeten Shift, indem sie je nach Taste die 32- oder 16-Bit-Taste umschalteten. Aus diesem Grund ist die Beziehung zwischen Klein- und Großbuchstaben in ASCII so regelmäßig, und die Beziehung zwischen Zahlen und Symbolen und einigen Symbolpaaren ist regelmäßig, wenn Sie darauf blinzeln. Mit dem ASR-33, bei dem es sich um ein Terminal in Großbuchstaben handelte, konnten Sie sogar einige Satzzeichen generieren, für die es keine Tasten gab, indem Sie das 16-Bit-Format verschieben. So wurde beispielsweise Shift-K (0x4B) zu einem [(0x5B)

ASCII wurde so konzipiert, dass die Tasten shiftund und ctrlohne viel (oder vielleicht auch keine ctrl) Logik implementiert werden können -shift wahrscheinlich sind nur wenige Gates erforderlich. Es war wahrscheinlich mindestens so sinnvoll, das Drahtprotokoll zu speichern wie jede andere Zeichenkodierung (keine Softwarekonvertierung erforderlich).

Der verlinkte Artikel auch erklärt , viele fremde Hacker Konventionen wie And control H does a single character and is an old^H^H^H^H^H classic joke.( gefunden hier ).

Iiridayn
quelle
1
Könnte einen Shift-Toggle für mehr ASCII mit implementieren foo ^= (foo & 0x60) == 0x20 ? 0x10 : 0x20, obwohl dies nur ASCII ist und daher aus Gründen, die in anderen Antworten angegeben sind, unklug ist. Es kann wahrscheinlich auch mit verzweigungsfreier Programmierung verbessert werden.
Iiridayn
1
Ah, foo ^= 0x20 >> !(foo & 0x40)wäre einfacher. Auch ein gutes Beispiel dafür, warum knapper Code oft als unlesbar angesehen wird ^ _ ^.
Iiridayn
8

Beim Xoring mit 32 (00100000 in Binär) wird das sechste Bit (von rechts) gesetzt oder zurückgesetzt. Dies entspricht genau dem Addieren oder Subtrahieren von 32.

Yves Daoust
quelle
2
Eine andere Möglichkeit, dies zu sagen, besteht darin, dass XOR Add-Without-Carry ist.
Peter Cordes
7

Die alphabetischen Bereiche in Klein- und Großbuchstaben %32überschreiten im ASCII-Codierungssystem keine "Ausrichtungsgrenze".

Aus diesem Grund ist Bit 0x20der einzige Unterschied zwischen den Groß- / Kleinbuchstaben desselben Buchstabens.

Wenn dies nicht der Fall wäre, müssten Sie addieren oder subtrahieren 0x20, nicht nur umschalten, und für einige Buchstaben würde es eine Ausführung geben, um andere höhere Bits umzudrehen. (Und es würde keine einzige Operation geben, die umschalten könnte, und es wäre schwieriger, zunächst nach alphabetischen Zeichen zu suchen, da Sie | = 0x20 nicht zwingen könnten, lcase zu erzwingen.)


Verwandte Nur-ASCII-Tricks: Sie können nach einem alphabetischen ASCII-Zeichen suchen, indem Sie Kleinbuchstaben mit erzwingen c |= 0x20und dann prüfen, ob (ohne Vorzeichen) c - 'a' <= ('z'-'a'). Also nur 3 Operationen: OR + SUB + CMP gegen eine Konstante 25. Natürlich wissen Compiler, wie (c>='a' && c<='z') man so etwas für Sie optimiert , also sollten Sie den c|=0x20Teil höchstens selbst erledigen . Es ist ziemlich unpraktisch, alle erforderlichen Castings selbst durchzuführen, insbesondere um standardmäßige Ganzzahl-Promotions für signierte zu umgehen int.

unsigned char lcase = y|0x20;
if (lcase - 'a' <= (unsigned)('z'-'a')) {   // lcase-'a' will wrap for characters below 'a'
    // c is alphabetic ASCII
}
// else it's not

Siehe auch Konvertieren einer Zeichenfolge in C ++ in Großbuchstaben (SIMD-Zeichenfolge)toupper für ASCII, wobei der Operand für XOR mithilfe dieser Prüfung maskiert wird.)

Außerdem erfahren Sie, wie Sie auf ein Zeichenarray zugreifen und Kleinbuchstaben in Großbuchstaben ändern und umgekehrt (C mit SIMD-Eigenheiten und skalarem x86-Asm-Fall-Flip für alphabetische ASCII-Zeichen, wobei andere unverändert bleiben).


Diese Tricks sind meistens nur nützlich, wenn Sie eine Textverarbeitung mit SIMD (z. B. SSE2 oder NEON) von Hand optimieren, nachdem Sie überprüft haben, ob bei keinem der chars in einem Vektor das High-Bit gesetzt ist. (Und daher ist keines der Bytes Teil einer Multi-Byte-UTF-8-Codierung für ein einzelnes Zeichen, die möglicherweise unterschiedliche Inversen in Groß- / Kleinbuchstaben aufweist.) Wenn Sie welche finden, können Sie für diesen 16-Byte-Block oder für den Rest der Zeichenfolge auf den Skalar zurückgreifen.

Es gibt sogar einige Gebietsschemas, in denen toupper()oder tolower()auf einigen Zeichen im ASCII-Bereich Zeichen außerhalb dieses Bereichs erzeugt werden, insbesondere auf Türkisch, wo I ↔ ı und İ ↔ i. In diesen Gebietsschemas benötigen Sie eine differenziertere Prüfung oder versuchen wahrscheinlich gar nicht, diese Optimierung zu verwenden.


In einigen Fällen dürfen Sie jedoch ASCII anstelle von UTF-8 annehmen, z. B. Unix-Dienstprogramme mit LANG=C(dem POSIX-Gebietsschema), nichten_CA.UTF-8 oder was auch immer.

Aber wenn Sie überprüfen können, ob es sicher ist, können Sie toupperZeichenfolgen mittlerer Länge viel schneller als toupper()in einer Schleife aufrufen (wie 5x), und zuletzt habe ich mit Boost 1.58 getestet , viel schneller als das, boost::to_upper_copy<char*, std::string>()was dynamic_castfür jeden Charakter dumm ist .

Peter Cordes
quelle