Мнения о типовом наказании в C ++? - PullRequest
22 голосов
/ 06 декабря 2008

Мне интересно узнать о соглашениях для указателей / массивов типов в C ++. Вот пример использования, который у меня есть на данный момент:

Вычислить простую 32-битную контрольную сумму для двоичного двоичного объекта данных, обрабатывая его как массив 32-битных целых чисел (мы знаем, что его общая длина кратна 4), а затем суммируя все значения и игнорируя переполнение.

Я ожидаю, что такая функция будет выглядеть так:

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;
 }

Теперь у меня вопрос: что вы считаете «лучшим» способом конвертации data в udata?

В стиле C?

udata = (const uint32_t *)data

C ++ приведение, которое предполагает, что все указатели являются конвертируемыми?

udata = reinterpret_cast<const uint32_t *>(data)

C ++ приведёт это между произвольными типами указателей, используя промежуточный void*?

udata = static_cast<const uint32_t *>(static_cast<const void *>(data))

В ролях через союз?

union {
    const uint32_t *udata;
    const char *cdata;
};
cdata = data;
// now use udata

Я полностью понимаю, что это не будет 100% переносимое решение, но я ожидаю использовать его только на небольшом наборе платформ, где, как я знаю, оно работает (а именно, невыровненный доступ к памяти и допущения компилятора при наложении указателей). Что бы вы порекомендовали?

Ответы [ 5 ]

13 голосов
/ 06 декабря 2008

Что касается стандарта C ++, ответ litb является полностью правильным и наиболее переносимым. Приведение const char *data к const uint3_t *, будь то с помощью приведения типа C, static_cast или reinterpret_cast, нарушает правила строгого алиасинга (см. Понимание строгого алиасинга ). Если вы компилируете с полной оптимизацией, есть большая вероятность, что код будет неправильным.

Приведение через объединение (например, my_reint от litb), вероятно, является лучшим решением, хотя оно технически нарушает правило, согласно которому, если вы записываете в объединение через одного члена и читаете его через другого, это приводит к неопределенному поведению. Однако практически все компиляторы поддерживают это, и это приводит к ожидаемому результату. Если вы абсолютно хотите соответствовать стандарту 100%, используйте метод сдвига битов. В противном случае, я бы порекомендовал пройти через объединение, которое, вероятно, даст вам лучшую производительность.

5 голосов
/ 07 декабря 2008

Игнорируя эффективность, для простоты кода я бы сделал:

#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);
}

Мне также нравится последний ответ litb, который сдвигает каждый символ по очереди, за исключением того, что, поскольку char может быть подписан, я думаю, что ему нужна дополнительная маска:

checksum += ((data[i] && 0xFF) << shift[i % 4]);

Когда потенциальная проблема связана с типом punning, я предпочитаю не печатать pun, а не пытаться делать это безопасно. Если вы вообще не создаете псевдонимы-указатели разных типов, вам не нужно беспокоиться о том, что компилятор может сделать с псевдонимами, равно как и программист по обслуживанию, который просматривает ваши множественные static_casts через объединение.

Если вы не хотите выделять столько дополнительной памяти, тогда:

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;
}

Достаточная оптимизация полностью избавит от memcpy и дополнительной переменной uint32_t в gcc, и просто прочитает целочисленное значение без выравнивания, каким бы наиболее эффективным способом это ни было на вашей платформе, прямо из исходного массива. Я надеюсь, что то же самое верно и для других «серьезных» компиляторов. Но этот код теперь больше, чем у litb, поэтому сказать о нем немногое, кроме моего, легче превратить в шаблон функции, который будет работать так же хорошо с uint64_t, а мой работает как нативная последовательность, а не выбирает немного -endian.

Это, конечно, не полностью переносимо. Предполагается, что хранилище представления символов sizeof (uint32_t) соответствует хранилищу представления uin32_t так, как мы хотим. Это подразумевается под вопросом, поскольку в нем говорится, что одно можно «рассматривать как» другое. Endian-ness, то есть, является ли char 8-битным, и использует ли uint32_t все биты в своем представлении хранения, может явно помешать, но вопрос подразумевает, что они не будут.

1 голос
/ 29 ноября 2015

Есть мои пятьдесят центов - разные способы это сделать.

#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";
}
0 голосов
/ 12 октября 2011

Я знаю, что эта ветка некоторое время неактивна, но я решил опубликовать простую общую процедуру приведения типов для такого рода вещей:

// 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 );

Надеюсь, это кому-нибудь поможет!

0 голосов
/ 07 декабря 2008

Это похоже на пример из практики, когда использовать reinterpret_cast, все остальное даст вам тот же эффект без явности, которую вы получите от использования языковой конструкции для официального использования.

...