Ao portar o código legado para C++ 20, substituí literais de string (com o texto codificado em UTF-8 esperado) por literais de string UTF-8 (aquele prefixado com u8
).
Assim, tive um problema com sequências octais que usei no passado para codificar sequências UTF-8 byte por byte:
Embora
"\303\274"
fosse a codificação adequada de ü
,
u8"\303\274"
acabou em ü
.
Eu investiguei isso mais detalhadamente e encontrei em cppreference.com :
Para cada sequência de escape numérica , dada
v
como o valor inteiro representado pelo número octal ou hexadecimal que compreende a sequência de dígitos na sequência de escape, e T como o tipo de elemento da matriz do literal de string (veja a tabela acima ):
- Se
v
não exceder o intervalo de valores representáveis de T, então a sequência de escape contribui com uma única unidade de código com valorv
.
(ênfase minha)
Em minhas próprias palavras: Em literais de string UTF-8, sequências de escape octais ( \ooo
) e hexadecimais ( \xXX
) são interpretadas como pontos de código Unicode, semelhantes às sequências Unicode ( \uXXXX
e \UXXXXXXXX
).
Portanto, isso me pareceu razoável: para literais de string UTF-8, as sequências de escape Unicode devem ser preferidas às sequências octais de bytes (que usei no passado).
Por curiosidade (e para efeito de demonstração), fiz um pequeno teste no coliru e fiquei surpreso ao ver que com g++ -std=c++20
, as sequências octais ainda são interpretadas como bytes únicos. Tendo em mente a citação acima, cheguei à conclusão:
MSVC parece estar correto e g++ errado.
Fiz um MCVE que executei em meu Visual Studio 2019 local:
#include <iostream>
#include <string_view>
void dump(std::string_view text)
{
const char digits[] = "0123456789abcdef";
for (unsigned char c : text) {
std::cout << ' '
<< digits[c >> 4]
<< digits[c & 0xf];
}
}
#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__
int main()
{
DEBUG(const char* const text = "\344\270\255");
DEBUG(dump(text));
std::cout << '\n';
DEBUG(const char8_t* const u8text = u8"\344\270\255");
DEBUG(dump((const char*)u8text));
std::cout << '\n';
DEBUG(const char8_t* const u8textU = u8"\u4e2d");
DEBUG(dump((const char*)u8textU));
std::cout << '\n';
}
Saída para MSVC :
const char* const text = "\344\270\255";
dump(text);
e4 b8 ad
const char8_t* const u8text = u8"\344\270\255";
dump((const char*)u8text);
c3 a4 c2 b8 c2 ad
const char8_t* const u8textU = u8"\u4e2d";
dump((const char*)u8textU);
e4 b8 ad
(Por favor, observe que o dump do 1º e do 3º literal são idênticos, enquanto o segundo resulta em sequências UTF-8, interpretando cada sequência octal como ponto de código Unicode.)
O mesmo código executado no Compiler Explorer, compilado com g++ (13.2) :
const char* const text = "\344\270\255";
dump(text);
e4 b8 ad
const char8_t* const u8text = u8"\344\270\255";
dump((const char*)u8text);
e4 b8 ad
const char8_t* const u8textU = u8"\u4e2d";
dump((const char*)u8textU);
e4 b8 ad
O mesmo código executado no Compiler Explorer, compilado com clang (17.0.1) :
const char* const text = "\344\270\255";
dump(text);
e4 b8 ad
const char8_t* const u8text = u8"\344\270\255";
dump((const char*)u8text);
e4 b8 ad
const char8_t* const u8textU = u8"\u4e2d";
dump((const char*)u8textU);
e4 b8 ad
Demonstração no Compiler Explorer
Minha conclusão está correta de que o MSVC corrige de acordo com o padrão C++, em oposição a g++ e clang?
O que encontrei por pesquisa na web antes:
- C++20 com u8, char8_t e std::string
- Usando prefixos literais de string UTF-8 portável entre C++ 17 e C++ 20
Usar sequências de escape hexadecimais em vez de sequências octais não muda nada: Demo no Compiler Explorer .
Eu preferia as sequências octais um tanto incomuns, pois elas são limitadas a 3 dígitos, nenhum caractere não relacionado pode estendê-las involuntariamente - em oposição às sequências hexadecimais.
Atualizar:
Quando eu estava prestes a registrar um bug para MSVC, percebi que isso já estava feito:
sequências de escape em literais de string unicode são sobrecodificadas (não conforme => bug do compilador)