Opiniões sobre tipo trocadilhos em C ++?
-
19-08-2019 - |
Pergunta
Estou curioso sobre convenções para-trocadilhos tipo ponteiros / matrizes em C ++. Aqui é o caso de uso que tenho no momento:
Calcular uma soma de verificação simples de 32 bits ao longo de um sistema binário de dados, tratando-a como uma matriz de números inteiros de 32-bit (que sabemos o seu comprimento total é um múltiplo de 4), e, em seguida, soma-se todos os valores e ignorando transbordamento.
Eu esperaria uma função tão para ficar assim:
uint32_t compute_checksum(const char *data, size_t size)
{
const uint32_t *udata = /* ??? */;
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
checksum += udata[i];
return udata;
}
Agora, a pergunta que eu tenho é, o que você considera a "melhor" maneira de data
convertido ao udata
?
C-estilo elenco?
udata = (const uint32_t *)data
C ++ elenco que assume todos os ponteiros são conversíveis?
udata = reinterpret_cast<const uint32_t *>(data)
C ++ fundido que entre tipos de ponteiro arbitrárias usando void*
intermediário?
udata = static_cast<const uint32_t *>(static_cast<const void *>(data))
fundido através de uma união?
union {
const uint32_t *udata;
const char *cdata;
};
cdata = data;
// now use udata
Eu percebo perfeitamente que isso não será uma solução portátil de 100%, mas estou apenas esperando para usá-lo em um pequeno conjunto de plataformas onde eu sei que funciona (nomeadamente acessos à memória unaligned e suposições do compilador sobre aliasing ponteiro). O que você recomendaria?
Solução
Quanto padrão do C ++ está em causa, litb ' s resposta é completamente correto e mais portátil. Fundição const char *data
a um const uint3_t *
, quer seja através de um elenco de estilo C, static_cast
, ou reinterpret_cast
, quebra as regras aliasing estritas (ver Entendimento Strict Aliasing ). Se você compilar com otimização plena, há uma boa chance de que o código não vai para a coisa certa.
Fundição através de uma união (como my_reint
do litb) é provavelmente a melhor solução, embora não tecnicamente violar a regra de que, se você escrever para uma união através de um membro e lê-lo através de um outro, que resulta em um comportamento indefinido. No entanto, praticamente todos os compiladores suportar isto, e isso resulta no resultado esperado. Se você absolutamente desejo de acordo com o padrão 100%, ir com o método de deslocamento de bit. Caso contrário, eu recomendo ir com lançando através de uma união, que é susceptível de lhe dar um melhor desempenho.
Outras dicas
Ignorando a eficiência, a simplicidade de código que eu faria:
#include <numeric>
#include <vector>
#include <cstring>
uint32_t compute_checksum(const char *data, size_t size) {
std::vector<uint32_t> intdata(size/sizeof(uint32_t));
std::memcpy(&intdata[0], data, size);
return std::accumulate(intdata.begin(), intdata.end(), 0);
}
Eu também gosto última resposta de litb, o que muda a cada caractere, por sua vez, só que desde caractere pode ser assinado, eu acho que precisa de uma máscara extra:
checksum += ((data[i] && 0xFF) << shift[i % 4]);
Quando o tipo de trocadilhos é um problema em potencial, eu prefiro não digite pun, em vez de tentar fazê-lo com segurança. Se você não criar qualquer ponteiros alias de tipos distintos, em primeiro lugar, então você não precisa se preocupar com o que o compilador pode fazer com aliases, e nem o programador de manutenção que vê suas múltiplas static_casts através de uma união.
Se você não quer alocar memória tanta extra, então:
uint32_t compute_checksum(const char *data, size_t size) {
uint32_t total = 0;
for (size_t i = 0; i < size; i += sizeof(uint32_t)) {
uint32_t thisone;
std::memcpy(&thisone, &data[i], sizeof(uint32_t));
total += thisone;
}
return total;
}
otimização suficiente vai se livrar do memcpy ea variável uint32_t adicional inteiramente no gcc, e apenas ler um valor inteiro não alinhado, qualquer que seja a forma mais eficiente de fazer isso é na sua plataforma, em linha reta da matriz de origem. Eu espero que o mesmo é verdade para outros compiladores "sérios". Mas este código é agora maior do que o do litb, por isso não há muito a ser dito para ele que não seja o meu é mais fácil de se transformar em um modelo função que vai funcionar tão bem com uint64_t, e meu trabalha como endian-ness nativo em vez de pouco escolhendo -endian.
Este é, naturalmente, não totalmente portátil. Assume-se que a representação de armazenamento de sizeof (uint32_t) carboniza corresponde à representação de armazenamento de um uin32_t da maneira que queremos. Isso está implícito na pergunta, uma vez que afirma que um pode ser "tratado como" o outro. Endian-ness, seja um char é de 8 bits, e se uint32_t usa todos os bits em sua representação de armazenamento pode, obviamente, intrometer, mas a questão implica que eles não vão.
Não são os meus cinquenta centavos -. Diferentes maneiras de fazer isso
#include <iostream>
#include <string>
#include <cstring>
uint32_t compute_checksum_memcpy(const char *data, size_t size)
{
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
{
// memcpy may be slow, unneeded allocation
uint32_t dest;
memcpy(&dest,data+i,4);
checksum += dest;
}
return checksum;
}
uint32_t compute_checksum_address_recast(const char *data, size_t size)
{
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
{
//classic old type punning
checksum += *(uint32_t*)(data+i);
}
return checksum;
}
uint32_t compute_checksum_union(const char *data, size_t size)
{
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
{
//Syntax hell
checksum += *((union{const char* c;uint32_t* i;}){.c=data+i}).i;
}
return checksum;
}
// Wrong!
uint32_t compute_checksum_deref(const char *data, size_t size)
{
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
{
checksum += *&data[i];
}
return checksum;
}
// Wrong!
uint32_t compute_checksum_cast(const char *data, size_t size)
{
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
{
checksum += *(data+i);
}
return checksum;
}
int main()
{
const char* data = "ABCDEFGH";
std::cout << compute_checksum_memcpy(data, 8) << " OK\n";
std::cout << compute_checksum_address_recast(data, 8) << " OK\n";
std::cout << compute_checksum_union(data, 8) << " OK\n";
std::cout << compute_checksum_deref(data, 8) << " Fail\n";
std::cout << compute_checksum_cast(data, 8) << " Fail\n";
}
Eu sei que esta discussão tem sido inativo por um tempo, mas pensei que eu ia postar uma rotina de fundição genérico simples para esse tipo de coisa:
// safely cast between types without breaking strict aliasing rules
template<typename ReturnType, typename OriginalType>
ReturnType Cast( OriginalType Variable )
{
union
{
OriginalType In;
ReturnType Out;
};
In = Variable;
return Out;
}
// example usage
int i = 0x3f800000;
float f = Cast<float>( i );
Espero que ajude alguém!
Isto parece um exemplo de caso-book de quando usar reinterpret_cast
, qualquer outra coisa lhe dará o mesmo efeito sem a explicitação você começa de usar um construtor de linguagem para seu uso oficial.