Подтвердить что ты не робот

Мнения о типе-казни в С++?

Мне интересно, как использовать условные обозначения для указателей/массивов типа С++. Вот пример использования, который у меня есть на данный момент:

Вычислите простую 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

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

udata = reinterpret_cast<const uint32_t *>(data)

С++ задает то, что между произвольными типами указателей, используя промежуточный 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% -ным портативным решением, но я только ожидаю использовать его на небольшом наборе платформ, где я знаю, что он работает (а именно, недопустимые обращения к памяти и предположения компилятора на сглаживание указателя). Что бы вы порекомендовали?

4b9b3361

Ответ 1

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

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

Ответ 2

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

#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, за исключением того, что, поскольку char может быть подписан, я думаю, что ему нужна дополнительная маска:

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

Когда тип punning является потенциальной проблемой, я предпочитаю не печатать каламбур, а не пытаться сделать это безопасно. Если в первую очередь вы не создаете какие-либо псевдонимы указателей разных типов, вам не нужно беспокоиться о том, что компилятор может делать с псевдонимами, и ни один программист по обслуживанию, который видит ваши множественные 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 и просто прочитает целочисленное значение без знака, причем самый эффективный способ сделать это на вашей платформе, прямо из исходного массива. Надеюсь, что то же самое относится и к другим "серьезным" компиляторам. Но этот код теперь больше, чем литч, поэтому нет ничего, что можно было бы сказать по этому поводу, кроме моего, проще превратить в шаблон функции, который будет работать так же хорошо с uint64_t, а мой работает как родная энциклопедия, а не немного -endian.

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

Ответ 3

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

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

Ответ 4

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

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

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

Ответ 5

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