Jaka jest idea ^ = 32, polegająca na zamianie małych liter na duże i odwrotnie?

146

Rozwiązałem jakiś problem z siłami współrzędnymi. Zwykle najpierw sprawdzam, czy znak jest wielką, czy dolną literą angielską, a następnie odejmuję lub dodaję, 32aby przekonwertować go na odpowiednią literę. Ale znalazłem kogoś, kto zrobił ^= 32to samo. Oto ona:

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

Szukałem wyjaśnienia tego i nie dowiedziałem się. Więc dlaczego to działa?

Devon
źródło
5
pl.wikipedia.org/wiki/File:USASCII_code_chart.png Wskazówka: możesz dokonać konwersji @na `używając ^ 32.
KamilCuk
112
FWIW, to naprawdę nie działa. Działa dla tego konkretnego zestawu znaków, ale są inne zestawy, w których nie powinno być używane toupperi tolowerdo przełączania przypadków.
NathanOliver
7
czasami w przypadku konkursów online "idea" polega na napisaniu kodu w tak zaciemniony sposób, że nigdy nie przeszedłby poważnej recenzji;)
idclev 463035818
21
^ = przekształca wartość za pomocą XOR. Wielkie litery ASCII mają zero w odpowiednim bicie, a małe litery mają jedynkę. To powiedziawszy, proszę nie! Użyj odpowiednich procedur znakowych (Unicode), aby dokonać konwersji między małymi i dużymi literami. Era samego ASCII już dawno minęła.
Hans-Martin Mosner
14
Nie chodzi tylko o to, że działa tylko z niektórymi zestawami znaków. Nawet jeśli założymy, że cały świat jest w UTF-8 (co może być przynajmniej miłym utopijnym celem), działa on również tylko z 26 literami Ado Z. To dobrze, o ile zależy Ci tylko na angielskim (i nie używaj pisowni „naiwny”, słów takich jak „kawiarnia” lub nazw ze znakami diakrytycznymi…), ale świat nie jest tylko angielski.
ilkkachu

Odpowiedzi:

149

Rzućmy okiem na binarną tabelę kodów ASCII.

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

A 32 jest 0100000jedyną różnicą między małymi i wielkimi literami. Więc przełączenie tego bitu powoduje przełączenie wielkości liter.

Hanjoung Lee
źródło
49
„przełącza wielkość liter” * tylko dla ASCII
Mooing Duck
39
@Mooing tylko dla A-Za-z w ASCII. Mała litera „[” nie jest „{”.
dbkk
21
@dbkk {jest krótszy niż [, więc jest to „mała” litera. Nie? Ok, pokażę się: D
Peter Badida,
25
Ciekawostka: w obszarze 7-bitowym niemieckie komputery zostały przemapowane na [] {|} do ĘÖÜäöü, ponieważ potrzebowaliśmy bardziej znaków Umlauts niż te znaki, więc w tym kontekście {(ä) faktycznie było małą literą [(Ę).
Guntram Blohm wspiera Monikę
14
@GuntramBlohm Kolejna ciekawostka, dlatego serwery IRC traktują foobar[] i foobar{}są identycznymi pseudonimami, ponieważ pseudonimy nie uwzględniają wielkości liter , a IRC ma swoje korzenie w Skandynawii :)
ZeroKnight,
117

Wykorzystuje to fakt, że wartości ASCII zostały wybrane przez naprawdę inteligentnych ludzi.

foo ^= 32;

Ten odwraca 6. najniższy bit 1 o foo(wielkie litery flagi ASCII rodzaju), transformujący ASCII górnej pokrywy do dolnej obudowy i odwrotnie .

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

Przykład

'A' ^ 32

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

I mienia XOR 'a' ^ 32 == 'A'.

Ogłoszenie

C ++ nie musi używać ASCII do reprezentowania znaków. Innym wariantem jest EBCDIC . Ta sztuczka działa tylko na platformach ASCII. Bardziej przenośnym rozwiązaniem byłoby użycie std::toloweri std::toupper, z oferowanym bonusem, znajomość języka (nie rozwiązuje to jednak automagicznie wszystkich problemów, patrz komentarze):

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) Ponieważ 32 to 1 << 5(2 do potęgi 5), odwraca szósty bit (licząc od 1).

YSC
źródło
16
EBCDIC został również wybrany przez bardzo sprytnych ludzi: działa naprawdę ładnie na kartach perforowanych por. ASCII, który jest bałaganem. Ale to dobra odpowiedź, +1.
Batszeba
65
Nie wiem o kartach perforowanych, ale ASCII został użyty na taśmie papierowej. Dlatego znak usuwania jest zakodowany jako 1111111: Możesz więc oznaczyć dowolny znak jako „usunięty”, wybijając wszystkie otwory w jego kolumnie na taśmie.
dan04,
23
@Bathsheba jako ktoś, kto nie używał karty dziurkowanej, bardzo trudno mi pojąć, że EBCDIC został inteligentnie zaprojektowany.
Lord Farquaad
9
@LordFarquaad IMHO Wikipedia obrazka tego, jak litery są zapisywane na karcie dziurkowanej, jest oczywistą ilustracją tego, jak EBCDIC ma jakiś (ale nie całkowity, zobacz / vs S) sens dla tego kodowania. en.wikipedia.org/wiki/EBCDIC#/media/…
Peteris
11
@ dan04 Uwaga, aby wspomnieć „jaka jest forma małej litery„ MASA ”?”. Dla tych, którzy nie wiedzą, są dwa słowa w języku niemieckim, których wielkimi literami jest MASA; jeden to „Masse”, a drugi to „Maße”. Prawidłowe tolowerw języku niemieckim nie wymaga jedynie słownika, musi być w stanie przeanalizować znaczenie.
Martin Bonner wspiera Monikę
35

Pozwólcie, że powiem, że jest to - choć wydaje się sprytne - naprawdę głupie hack. Jeśli ktoś ci to poleci w 2019 roku, uderz go. Uderz go tak mocno, jak potrafisz.
Możesz oczywiście zrobić to we własnym oprogramowaniu, którego Ty i nikt inny nie używa, jeśli wiesz, że i tak nigdy nie będziesz używać żadnego języka poza angielskim. W przeciwnym razie nie idź.

Hack był dyskusyjny "OK" jakieś 30-35 lat temu, kiedy komputery tak naprawdę niewiele robiły, poza angielskim w ASCII i może w jednym lub dwóch głównych językach europejskich. Ale ... już nie.

Hack działa, ponieważ wielkie i małe litery w języku amerykańskim i łacińskim są dokładnie 0x20od siebie oddalone i pojawiają się w tej samej kolejności, co stanowi tylko niewielką różnicę. Co w rzeczywistości, ten kawałek włamuje się.

Teraz ludzie tworzący strony kodowe dla Europy Zachodniej, a później konsorcjum Unicode, byli na tyle sprytni, aby zachować ten schemat np. Dla niemieckich umlautów i samogłosek z francuskim akcentem. Nie tak dla ß, które (dopóki ktoś nie przekonał konsorcjum Unicode w 2017 r., A duży drukowany magazyn Fake News napisał o tym, faktycznie przekonując Dudena - bez komentarza) nie istnieje nawet jako wersal (przekształca się w SS) . Teraz to ma istnieć jako Versal, ale dwa są 0x1DBFpozycje od siebie, nie 0x20.

W implementors były jednak nie taktowny tyle, aby utrzymać ten zamiar. Na przykład, jeśli zastosujesz hack w niektórych językach wschodnioeuropejskich lub podobnych (nie wiedziałbym o cyrylicy), otrzymasz paskudną niespodziankę. Wszystkie te „toporki” są tego przykładem, małe i duże litery są jednym z osobna. Dlatego hack nie działa tam poprawnie.

Należy wziąć pod uwagę znacznie więcej, na przykład niektóre znaki w ogóle nie zmieniają się z małych na duże (są zastępowane różnymi sekwencjami) lub mogą zmieniać formę (wymagając różnych punktów kodowych).

Nawet nie myśl o tym, co ten hack zrobi z rzeczami takimi jak tajskie lub chińskie (po prostu dostaniesz kompletny nonsens).

Oszczędność kilkuset cykli procesora mogła być bardzo opłacalna 30 lat temu, ale obecnie nie ma usprawiedliwienia dla prawidłowej konwersji łańcucha. Istnieją funkcje biblioteczne do wykonania tego nietrywialnego zadania.
Czas potrzebny do konwersji kilkadziesiąt kilobajtów tekstu właściwie jest znikoma w dzisiejszych czasach.

Damon
źródło
2
Całkowicie się zgadzam - chociaż każdy programista powinien wiedzieć, dlaczego to działa - może nawet zadać dobre pytanie do wywiadu. Co to robi i kiedy powinno być używane :)
Bill K
33

Działa, ponieważ tak się składa, że ​​różnica między „a” i A ”w kodowaniu ASCII i pochodnych wynosi 32, a 32 to także wartość szóstego bitu. Odwrócenie szóstego bitu z wyłącznym OR powoduje więc konwersję między górnym a dolnym.

Jack Aidley
źródło
22

Najprawdopodobniej Twoją implementacją zestawu znaków będzie ASCII. Jeśli spojrzymy na stół:

wprowadź opis obrazu tutaj

Widzimy, że istnieje różnica dokładnie 32między wartością małej i dużej litery. Dlatego jeśli to zrobimy ^= 32(co jest równoznaczne z przełączeniem szóstego najmniej znaczącego bitu), zmienia się między małą a wielką literą.

Zauważ, że działa ze wszystkimi symbolami, a nie tylko literami. Przełącza znak z odpowiednim znakiem, w którym szósty bit jest inny, w wyniku czego para znaków jest przełączana między nimi. W przypadku liter odpowiednie duże / małe litery tworzą taką parę. A NULzmieni się w Spacei na odwrót, i @przełącza się z backtickiem. Zasadniczo każdy znak w pierwszej kolumnie na tym wykresie jest przełączany ze znakiem znajdującym się o jedną kolumnę powyżej i to samo dotyczy trzeciej i czwartej kolumny.

Nie użyłbym jednak tego hacka, ponieważ nie ma gwarancji, że zadziała na każdym systemie. Po prostu użyj zamiast tego toupper i tolower oraz zapytań takich jak isupper .

Płomień
źródło
2
Cóż, nie działa dla wszystkich liter, które mają różnicę 32. W przeciwnym razie działałoby między „@” a „”!
Matthieu Brucher
2
@MatthieuBrucher To działa, 32 ^ 32jest 0, a nie 64
NathanOliver
5
„@” i „” nie są „literami”. Tylko [a-z]i [A-Z]są „literami”. Reszta to zbiegi okoliczności, na których obowiązuje ta sama zasada. Gdyby ktoś poprosił cię o „wielkie litery]”, co by to było? nadal byłoby to „]” - „}” nie jest „wielką literą” w „]”.
wolność, n-m,
4
@MatthieuBrucher: Innym sposobem na podkreślenie tego jest to, że zakresy małych i wielkich liter alfabetu nie przekraczają %32granicy „wyrównania” w systemie kodowania ASCII. To dlatego trochę 0x20to jedyna różnica pomiędzy górnymi / niższych wersjach przypadku tego samego listu. Gdyby tak nie było, musiałbyś dodawać lub odejmować 0x20, a nie tylko przełączać, a dla niektórych liter byłoby wykonanie, aby przerzucić inne wyższe bity. (I ta sama operacja nie mogłaby się przełączyć, a sprawdzenie znaków alfabetycznych w pierwszej kolejności byłoby trudniejsze, ponieważ nie można było |= 0x20wymusić lcase.)
Peter Cordes,
2
+1 za przypominanie mi o wszystkich tych wizytach w asciitable.com, aby wpatrywać się w tę dokładną grafikę (i rozszerzoną wersję ASCII !!) przez ostatnie, nie wiem, 15 czy 20 lat?
AC
15

Wiele dobrych odpowiedzi, które opisują, jak to działa, ale dlaczego tak działa, to poprawa wydajności. Operacje bitowe są szybsze niż większość innych operacji w procesorze. Możesz szybko przeprowadzić porównanie bez uwzględniania wielkości liter, po prostu nie patrząc na bit, który określa wielkość liter lub zmieniając wielkość liter na duże / dolne, po prostu odwracając bit (ci faceci, którzy zaprojektowali tabelę ASCII, byli całkiem sprytni).

Oczywiście nie jest to dziś tak wielka transakcja, jak to było w 1960 roku (kiedy po raz pierwszy rozpoczęto prace nad ASCII) ze względu na szybsze procesory i Unicode, ale nadal istnieje kilka tanich procesorów, które mogą mieć znaczący wpływ tak długo, jak możesz zagwarantować tylko znaki ASCII.

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

Na prostych, niedrogich procesorach operacje bitowe są zwykle znacznie szybsze niż dzielenie, kilka razy szybciej niż mnożenie, a czasami znacznie szybciej niż dodawanie.

UWAGA: Zalecałbym używanie standardowych bibliotek do pracy z napisami z kilku powodów (czytelność, poprawność, przenośność itp.). Używaj odwracania bitów tylko wtedy, gdy zmierzyłeś wydajność i to jest twoje wąskie gardło.

Brian
źródło
14

Tak działa ASCII, to wszystko.

Ale wykorzystując to, rezygnujesz z przenośności, ponieważ C ++ nie nalega na ASCII jako kodowanie.

Dlatego funkcje std::toupperi std::tolowersą zaimplementowane w standardowej bibliotece C ++ - zamiast tego należy ich używać.

Batszeba
źródło
6
Istnieją jednak protokoły, które wymagają użycia ASCII, takie jak DNS. W rzeczywistości „sztuczka 0x20” jest używana przez niektóre serwery DNS w celu wstawienia dodatkowej entropii do zapytania DNS jako mechanizmu zapobiegającego fałszowaniu. DNS nie rozróżnia wielkości liter, ale powinien również zachowywać wielkość liter, więc jeśli wyślesz zapytanie z losową wielkością liter i otrzymasz tę samą wielkość liter, to dobrze wskazuje, że odpowiedź nie została sfałszowana przez osobę trzecią.
Alnitak
Warto wspomnieć, że wiele kodowań nadal ma tę samą reprezentację dla standardowych (nie rozszerzonych) znaków ASCII. Jeśli jednak naprawdę martwisz się różnymi kodowaniami, powinieneś użyć odpowiednich funkcji.
Captain Man
5
@CaptainMan: Oczywiście. UTF-8 to rzecz po prostu piękna. Miejmy nadzieję, że zostanie „wchłonięty” przez standard C ++ w takim stopniu, w jakim IEEE754 ma dla zmiennoprzecinkowych.
Batszeba
11

Zobacz drugą tabelę pod adresem http://www.catb.org/esr/faqs/things-every-hacker-once-knew/#_ascii oraz poniższe uwagi, zamieszczone poniżej:

Modyfikator Control na klawiaturze w zasadzie czyści trzy górne bity dowolnego wpisanego znaku, pozostawiając pięć dolnych i mapując je na zakres 0..31. Na przykład Ctrl-SPACE, Ctrl- @ i Ctrl-`oznaczają to samo: NUL.

Bardzo stare klawiatury wykonywały Shift tylko przez przełączanie 32 lub 16 bitów, w zależności od klawisza; dlatego związek między małymi i dużymi literami w ASCII jest tak regularny, a związek między liczbami i symbolami oraz niektórymi parami symboli jest dość regularny, jeśli się na to zmrużysz. ASR-33, który był terminalem składającym się wyłącznie z wielkich liter, pozwolił nawet wygenerować kilka znaków interpunkcyjnych, dla których nie miał kluczy, przez przesunięcie 16-bitowego; w ten sposób, na przykład, Shift-K (0x4B) stało się [(0x5B)

ASCII został zaprojektowany w taki sposób, że klawisze shifti ctrlklawiatury można było zaimplementować bez dużej (lub być może żadnej ctrl) logiki - shiftprawdopodobnie wymagało to tylko kilku bramek. Prawdopodobnie przechowywanie protokołu przewodowego miało co najmniej tyle samo sensu, co inne kodowanie znaków (nie jest wymagana konwersja oprogramowania).

Połączony artykuł wyjaśnia również wiele dziwnych konwencji hakerskich, takich jak And control H does a single character and is an old^H^H^H^H^H classic joke.( znalezione tutaj ).

Iiridayn
źródło
1
Może zaimplementować przełącznik zmiany biegów dla większej liczby znaków ASCII w / foo ^= (foo & 0x60) == 0x20 ? 0x10 : 0x20, chociaż jest to tylko ASCII, a zatem nierozsądne z powodów podanych w innych odpowiedziach. Prawdopodobnie można to również ulepszyć w programowaniu bez gałęzi.
Iiridayn,
1
Ach, foo ^= 0x20 >> !(foo & 0x40)byłoby prostsze. Również dobry przykład, dlaczego zwięzły kod jest często uważany za nieczytelny ^ _ ^.
Iiridayn,
8

Xoring z 32 (00100000 binarnie) ustawia lub resetuje szósty bit (od prawej). Jest to ściśle równoważne z dodawaniem lub odejmowaniem 32.

Yves Daoust
źródło
2
Innym sposobem na powiedzenie tego jest to, że XOR jest dodawany bez przenoszenia.
Peter Cordes,
7

Zakresy pisane małymi i dużymi literami alfabetu nie przekraczają %32granicy „wyrównania” w systemie kodowania ASCII.

Dlatego bit 0x20jest jedyną różnicą między wersjami tej samej litery z dużych / małych liter.

Gdyby tak nie było, musiałbyś dodawać lub odejmować 0x20, a nie tylko przełączać, a dla niektórych liter byłoby przeprowadzane, aby przerzucić inne wyższe bity. (I nie byłoby ani jednej operacji, która mogłaby się przełączać, a sprawdzenie znaków alfabetu w pierwszej kolejności byłoby trudniejsze, ponieważ nie można | = 0x20 wymusić lcase.)


Powiązane sztuczki tylko ASCII: możesz sprawdzić alfabetyczny znak ASCII , wymuszając małe litery za pomocą, c |= 0x20a następnie sprawdzając, czy (bez znaku) c - 'a' <= ('z'-'a'). Więc tylko 3 operacje: OR + SUB + CMP w porównaniu ze stałą 25. Oczywiście kompilatory wiedzą, jak zoptymalizować (c>='a' && c<='z') dla Ciebie taki asm , więc co najwyżej powinieneś wykonać tę c|=0x20część samodzielnie. Raczej niewygodne jest samodzielne wykonywanie wszystkich niezbędnych rzutów, zwłaszcza w celu obejścia domyślnych promocji całkowitych na podpisane 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

Zobacz także Konwertuj ciąg w C ++ na wielkie litery (ciąg SIMD touppertylko dla ASCII, maskujący operand dla XOR przy użyciu tego sprawdzenia).

A także jak uzyskać dostęp do tablicy znaków i zmienić małe litery na duże i odwrotnie (C z elementami wewnętrznymi SIMD i skalarnym odwracaniem wielkości liter x86 asm dla alfabetycznych znaków ASCII, pozostawiając inne niezmodyfikowane).


Te triki są głównie przydatne tylko przy ręcznej optymalizacji przetwarzania tekstu za pomocą SIMD (np. SSE2 lub NEON), po sprawdzeniu, że żaden z chars w wektorze nie ma ustawionego wysokiego bitu. (A zatem żaden z bajtów nie jest częścią wielobajtowego kodowania UTF-8 dla pojedynczego znaku, który może mieć różne odwrotne wielkie / małe litery). Jeśli znajdziesz, możesz wrócić do wartości skalarnej dla tego fragmentu 16-bajtowego lub dla reszty ciągu.

Istnieją nawet ustawienia narodowe, w których toupper()lub tolower()na niektórych znakach z zakresu ASCII tworzą znaki spoza tego zakresu, zwłaszcza tureckie, gdzie I ↔ ı i İ ↔ i. W tych lokalizacjach potrzebowałbyś bardziej wyrafinowanej kontroli lub prawdopodobnie nie próbowałbyś w ogóle używać tej optymalizacji.


Ale w niektórych przypadkach możesz założyć ASCII zamiast UTF-8, np. Narzędzia Unix z LANG=C(locale POSIX), nie en_CA.UTF-8lub cokolwiek.

Ale jeśli możesz zweryfikować, że jest to bezpieczne, możesz toupperznacznie szybciej łańcuchy średniej długości niż wywoływanie toupper()w pętli (np. 5x), a ostatnio testowałem z Boost 1.58 , znacznie szybciej niż boost::to_upper_copy<char*, std::string>()robi to głupio dynamic_castdla każdej postaci.

Peter Cordes
źródło