Мнения о каламбуре в C ++?

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

Compute a simple 32-bit checksum over a binary blob of data by treating it as an array of 32-bit integers (we know its total length is a multiple of 4), and then summing up all values and ignoring overflow.

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

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 вы считаете «лучшим»?

С-стиль?

udata = (const uint32_t *)data

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

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


person Tom    schedule 06.12.2008    source источник


Ответы (4)


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

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

person Adam Rosenfield    schedule 06.12.2008
comment
Решения litb верны по стандарту, но, как я уже сказал, я уже смотрю на конкретные платформы. - person Tom; 07.12.2008
comment
Я не уверен, почему они опускают это :), но я также не уверен, что мое использование объединения является неопределенным поведением. Я знаю, что запись в член и чтение из другого члена являются неопределенным поведением. но в моем случае , я указываю на его член, который, как предполагается, имеет допустимое значение, и затем читаю его. - person Johannes Schaub - litb; 07.12.2008
comment
Я не думаю, что этот конкретный пример нарушает строгий псевдоним. char * - это особый случай в соответствии со строгими правилами псевдонимов - char * никогда не может считаться псевдонимом указателя на какой-либо другой тип. Но в своем ответе я все же перестраховываюсь: просто не стоит делать char * иначе, чем в других подобных случаях. - person Steve Jessop; 07.12.2008
comment
onebyone, типичный каламбур - это не проблема, это уже решено профсоюзом. но проблема заключается в чтении члена профсоюза, даже если мы не писали ему раньше. стандарт, кажется, не запрещает это. но в этом вопросе мы не уверены: / - person Johannes Schaub - litb; 07.12.2008
comment
@ Адам, я решил удалить свой ответ, потому что обнаружил, что больше не согласен с большинством его интерпретаций. Но я по-прежнему согласен с вашим мнением в этом ответе. - person Johannes Schaub - litb; 23.05.2009
comment
@litb: Я имел в виду (если я могу вспомнить то время): вопреки тому, что говорит Адам, приведение const char* к const uint32_t* не нарушает строгие правила псевдонима. Сопоставление uint32_t* с char* разрешено и безопасно, и оптимизация этого не меняет. - person Steve Jessop; 20.11.2009
comment
@SteveJessop, приведение к const char * разрешено строгим псевдонимом, но приведение к uint32_t * не допускается строгим псевдонимом. Пояснение к пункту 5 в en.cppreference.com/w/cpp/language/reinterpret_cast показывает это с T2 в качестве цели. В разделе Type Aliasing ниже указано, что T2 должен быть char или unsigned char. Итак, преобразование в unsigned int * действительно запрещено строгим псевдонимом. - person D. A.; 06.10.2014
comment
memcpy оптимизируется с помощью обычных компиляторов (по крайней мере, когда целевая ISA поддерживает невыровненные загрузки / сохранения, что в наши дни делают наиболее важные из них) и является безопасным способом выполнения невыровненной загрузки с псевдонимом. (или магазин). Союзы небезопасны в C ++; несколько реальных компиляторов, таких как Sun, на практике нарушают идиому объединения. blog.regehr.org/archives/959 В C ++ 20 вы можете используйте std::bit_cast между массивом char[4], но memcpy, вероятно, на самом деле проще, если вы хотите читать несколько символов, а не каламбур между элементами одинакового размера. - person Peter Cordes; 22.03.2021

Не обращая внимания на эффективность, для простоты кода я бы сделал:

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

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

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

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, а мой работает как нативный порядок байтов, а не выбирает мало -энди.

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

person Steve Jessop    schedule 07.12.2008
comment
Я только что попробовал твой последний пример. GCC отказывается векторизовать его, жалуясь на необработанные данные-ref. - person Tom; 07.12.2008
comment
Честно говоря, это не самый быстрый вариант на оборудовании, поддерживающем векторные операции. Возможно, в будущем GCC станет лучше. Мой гуру экстремального программирования говорит, что мне не нужно терять сон из-за этого. Мое первое предложение работает не так быстро на любом оборудовании :-) - person Steve Jessop; 07.12.2008
comment
Но я согласен с тем, что, поскольку я упомянул о низкой стоимости memcpy, тот факт, что он предотвращает векторизацию, может стать препятствием для некоторых приложений. - person Steve Jessop; 07.12.2008
comment
К сожалению, 8 лет спустя GCC все еще не векторизует его, в отличие от версии с типографским шрифтом ... - person Ruslan; 26.08.2016
comment
GCC теперь также векторизует версию memcpy. - person Dino; 15.11.2019
comment
memcpy по одному uint32 за раз в локальную оптимизацию. Копирование всех байтов одним большим memcpy, особенно в динамически выделяемое хранилище (std :: vector), скорее всего, не будет. Или в GNU C используйте typedef uint32_t aliasing_unaligned_u32 __attribute__((aligned(1),may_alias)), как в Почему strlen glibc должен быть таким сложным для быстрого запуска?. В любом случае, первая рекомендация в вашем ответе, большой memcpy, явно очень плохая и должна быть удалена. - person Peter Cordes; 22.03.2021
comment
(Честно говоря, при компиляции для платформы без невыровненных загрузок с одной инструкцией в asm (например, классический MIPS), даже 4-байтовый memcpy может не встроиться в GCC, и в этом случае одна большая копия в std :: vector может быть меньшее из двух зол, если бы вам пришлось выбирать между этими двумя, хотя, конечно, не оптимальное решение asm.) - person Peter Cordes; 22.03.2021

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

#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";
}
person Петър Петров    schedule 29.11.2015
comment
-1 для старых приведений C и не указывать, что каламбур с помощью union является неопределенным поведением, поскольку только последний элемент, к которому был осуществлен доступ, имеет значение, определенное стандартом - person underscore_d; 31.12.2015
comment
*(uint32_t*)(data+i); небезопасно даже в C (кроме MSVC или gcc -fno-strict-aliasing). Кроме того, каламбур типа union помещает в объединение указатель, а не данные! Вы хотите объединить {char c[4]; uint32_t u;}, если хотите каламбур типа объединения C99. (Который поддерживается в C ++ многими, но не всеми реальными реализациями. Включая G ++ / clang ++, но я думаю, что слышал не о компиляторе Sun). - person Peter Cordes; 22.03.2021

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

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

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

person Hybrid    schedule 12.10.2011
comment
Типовой каламбур в стандарте не определен. Однако типирование с использованием объединений поддерживается по крайней мере GCC (с включенным строгим алиасингом iirc). - person Jens Åkerblom; 18.05.2013
comment
Это не определено? Я понимаю, что это неуказанное поведение, только если размеры типов различаются, а не undefined. Утверждают, что размер совпадает, в целях безопасности. (stackoverflow.com/questions/11639947/) - person Hybrid; 23.05.2013
comment
@Hybrid этот ответ касается C, а не C ++. Поведение в C и C ++ радикально отличается. - person The Paramagnetic Croissant; 02.05.2014
comment
Типовой каламбур, используемый в объединениях, вполне допустим и работает во всех компиляторах: GCC, MSVC, LLVM. Однако вам придется решить только одну проблему: порядок байтов. В этом вам помогут маски байтового порядка. - person Петър Петров; 29.11.2015
comment
@ ПетърПетров, нет, стой. совершенно допустимый и рабочий случай в случае, определяемом реализацией этих трех компиляторов, которые я тестировал, не приравнивается к определенному, гарантированному, переносимому, совместимому со стандартом поведению. Опора на это поведение, определяемое реализацией, становится огромным риском для переносимости, который в большинстве случаев действительно не стоит брать. Конечно, не указывайте читателям, что это безопасно. - person underscore_d; 31.12.2015
comment
@ JensÅkerblom Больше, чем IYRC, есть ли у вас ссылка, где g++ это предусмотрено? Я полагаюсь на поведение, определяемое реализацией, если я должен, но мне нужно знать, что оно где-то определено. - person underscore_d; 05.01.2016
comment
Мое плохое, правильный термин здесь определенно не совсем верный и рабочий случай, это только идеально рабочий случай в реализациях упомянутых компиляторов, И вы должны сами иметь дело с порядком байтов! Что касается g ++, я не думаю, что он определен, но этот компилятор уже компилирует множество игр, в которых используется объединение типов, как и LLVM. - person Петър Петров; 07.01.2016
comment
@underscore_d: Выбор типа через объединение - настолько важная идиома как в C, так и в C ++, что не представляет большого риска для переносимости, независимо от того, что говорят стандарты. - person Erik Alapää; 30.01.2017
comment
@underscore_d: gcc.gnu.org / on Lineledocs / gcc / подтверждает, что GNU C90 определяет поведение каламбура типа union, а не только C99 и более поздние версии. (Также относится к диалекту GNU C ++, как указано в gcc .gnu.org / onlinedocs / gcc / Optimize-Options.html # Type-punning). blog.regehr.org/archives/959 подтверждает, что GNU C ++ (включая clang) создает тип объединения -беговой сейф. Но также указывает, что некоторые реальные реализации C ++ не делают его безопасным, особенно компилятор Sun. К счастью, в C ++ 20 наконец-то появился std::bit_cast<> - person Peter Cordes; 22.03.2021