-2

I am trying to create a bit-field struct which has 32 total bits, but when I try to assign a 32-bit number to it, I get this error:

Implicit truncation from 'unsigned int' to bit-field changes value from 4278190080 to 0

Here is my struct and how I'm trying to use it

struct Color32 {
    uint32_t a : 8;
    uint32_t r : 8;
    uint32_t g : 8;
    uint32_t b : 8;
};


Color32 BLACK = {0xFF000000}; // this line has the compilation error

I see other questions around bit-field assignment, but they all seem to use bit-wise operations to set the individual fields.

There's also this reference which has the following sample, which seems to be the same way I'm using it, only mine won't compile:

#include <iostream>
struct S {
 // three-bit unsigned field,
 // allowed values are 0...7
 unsigned int b : 3;
};
int main()
{
    S s = {6};
    ++s.b; // store the value 7 in the bit field
    std::cout << s.b << '\n';
    ++s.b; // the value 8 does not fit in this bit field
    std::cout << s.b << '\n'; // formally implementation-defined, typically 0
}
Kyle Falconer
  • 8,302
  • 6
  • 48
  • 68
  • Ok, so you've defined a struct with bit field size of n. Of Course some constructors would be necessary for the top most implementation. What i am unclear of is if you need 8 bits fixed size, why use multiple `32` bit integers? If you are trying to make processor instructions, why not make structured union ? – Danilo Aug 15 '19 at 23:07
  • Maybe something along the lines of this would help: https://stackoverflow.com/questions/3065948/c-unions-bit-fields-task – Danilo Aug 15 '19 at 23:13
  • What's wrong with using operations to set the individual fields? That lets you control where the different parts of the initialization data go, without having to experiment. – JaMiT Aug 16 '19 at 02:33

4 Answers4

7

You could use aggregate initialization here

Color32 BLACK = {0xFF, 0x00, 0x00, 0x00};

By the way, I would suggest modifying your Color32 struct to the following, which will have the same effect as specifying the bit field of your members

struct Color32 {
    uint8_t a;
    uint8_t r;
    uint8_t g;
    uint8_t b;
};
Cory Kramer
  • 114,268
  • 16
  • 167
  • 218
  • 1
    I would just like to add to this ( since it seems OP wants an `uint32_t` behaviour), you can then make a constructor that accepts an `uint32_t` as input parameter , and use `operator uint32_t()` overload to make it function like an regular integer. – Danilo Aug 16 '19 at 00:09
  • I had started with the `uint8_t` members as well, but started overthinking it to the point where I asked the question "why shouldn't I be able to set this all as one integer?" However, I think your answer is likely the best approach, while avoiding undefined behavior and being more portable. – Kyle Falconer Aug 16 '19 at 18:12
5

Something like this will sort of give you the best of both worlds:

    struct Color32 {
    union {
        uint32_t color;
        struct {
            uint32_t b : 8;
            uint32_t g : 8;
            uint32_t r : 8;
            uint32_t a : 8;
        };
    };
};

// will construct using single value
Color32 test{ 0x01020304 };
Color32 black{0xff000000 };

// can assign to individual fields
test.a = 0x04;
test.r = 0x03;
test.g = 0x02;
test.b = 0x01;

// can assign to the whole value like this.
test.color = 0xff000000;
test.color = black.color;

An issue with this is that the order of a, b, g, r in the struct may be dependent upon your specific compiler. For VS2017 compiling to windows target, the order shown will produce the expected results. I believe there may be a way to force the order somehow, but I am not familiar with how to do it.

ttemple
  • 852
  • 5
  • 20
  • Great minds think alike. The way to force endianness is to chose one and stick to it. which means detecting the OS endians by macro or function, and manipulating data no matter bit/little or mid endians. That is why unions and pointers are awesome. You can swap data ( and keep them contained in type space ) with no issue. You can do similar thing here ( a becomes b, r becomes g ) but it isn't fun to keep track. So often proxies structs are used not to mess up the structure. – Danilo Aug 15 '19 at 23:48
  • 1
    @Danilo - the portability zealots hate this kind of stuff. For me, my targets are fixed and I do this kind of stuff all the time. I suppose someday it will backfire on me. – ttemple Aug 15 '19 at 23:50
  • 1
    Well if you code for your machine only, it shouldn't be a problem. But if you wish to use protocols that speak with the server ( IP,UDP.... ) you would need to use different server side and client side code. If you have no use for internet communication , you are golden :D But if the need ever arises :https://stackoverflow.com/a/1001373/3142882 – Danilo Aug 16 '19 at 00:00
  • That is very nice. I'll probably use that somewhere. – ttemple Aug 16 '19 at 00:02
  • Another issue with this is that you can easily get undefined behavior. Not only does the compiler get to pick the order of fields in the structure, but it also decides if the following sequence of statements is valid [(it's officially undefined behavior)](https://en.cppreference.com/w/cpp/language/union#Explanation): `black.color = 0xff000000; test.a = black.a;` . – JaMiT Aug 16 '19 at 02:41
  • @JaMiT, point well taken. I work a lot with dedicated compilers for specific hardware. In some cases the hardware has bitfield instructions that make it very compelling to use bitfields. It always takes some experimenting to detect the alignment and endian issues. I am making the assumption that toolchain updates will not break things later, but it is certainly possible. For me the clarity that it adds to the code over using bit masks and shifts is worth the cost of the slight risk of it breaking on a compiler update. – ttemple Aug 16 '19 at 17:35
  • @ttemple It is reasonable-ish to assume that a C++ compiler will not choose to drop support for C's [type punning via `union`](https://en.wikipedia.org/wiki/Type_punning#Use_of_union). However, the question does not mention which compiler is being used, nor does it mention that only one compiler will be used. So isn't the possibility of undefined behavior a caveat that's as important to mention as potential struct layout issues? – JaMiT Aug 16 '19 at 23:17
0

Bitfield or not, your type has four members, not one.

You seem to be trying to treat it as a union.

Initialise each member individually as you would with any other type, or switch to a union (and then rely on type-punning as many do, with the usual caveats).


The counter-example you give is not the same, as it is a UDT with a single member and a single value in the initialiser; since the number of members given matches, everything is fine there.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
0

After diving more into the topic, i've found that multiple bit fields aren't useful without bitwise operators and valid constructors, with that it depends a lot on the operating system.

The answer is tested on cygwin ( -Wno-unused-variable -O0 -ggdb flags ) on windows 7

Version 1: union

This is basic implementation without any bit fields, most common implementation of 4 byte colour space.

#include <iostream>  

union c_space32{
    uint32_t space;
    uint8_t channels[4];
};

int main(){

    { // just a anonymous scope to keep things clear 
        union c_space32 temp = {0xff00fe32};
        std::cout << "sizeof : " << sizeof( union c_space32 ) << "\n\n";
        std::cout << (int)temp.channels[1] << "\t" << std::hex << temp.space <<  "\n";
        ++temp.channels[1];
        std::cout << (int)temp.channels[1] << "\t" << std::hex << temp.space <<  "\n";
        ++temp.channels[1];
        std::cout << (int)temp.channels[1] << "\t" << std::hex << temp.space <<  "\n";
    }

return 0;}  

The union behaves as normal color space, and every uint8_t part of the union behaves as unique byte, so overall change of value in c_space32.channels doesn't affect the value of c_space32.space outside of the scope of the byte. This is the output i am getting.

sizeof : 4
fe  ff00fe32
ff  ff00ff32
0   ff000032  

Version 2: bit-fields

The issue with bit fields ( among lack of documentation in some cases ), is that they can easily change in size, and that endianness depends on OS so native logic behind structure of bit fields can escape our human logic. Let me give you some examples for future guys/gals who wish to endeavour into this topic.

#include <iostream> 
#include <bitset> 

struct temp1{
    uint8_t a:1;
    temp1(uint8_t val){ // just so i can assign it
        this->a = (val & 0x1 ); // this is needed to avoid truncated warning
    }
};

int main(){
    struct temp1 t1 = 3;
    uint8_t *ptr = (uint8_t *)&t1;
    std::cout << sizeof(struct temp1) << std::endl; // size of 1 byte
    std::cout << std::bitset<8>( *ptr ) << std::endl; // 0000-0001 position of our bitfield
return 0;}

So in this case sizeof(struct temp1) returns an size of 1 byte. With the position of our bit field as upmost right. And here is where documentation starts to go MIA.

#include <iostream> 
#include <bitset> 

struct temp2{
    uint8_t a:1;
    uint8_t b:1;
    uint8_t c:1;
    temp2(int VAL){ // just so i can assign it
        this->a = (VAL & 0x1 );
        this->b = 0;
        this->c = (VAL >> 2 ) & 0x1;
    }
};

int main(){
    struct temp2 t1 = 0xf;
    uint8_t *ptr = (uint8_t *)&t1;
    std::cout << sizeof(struct temp2) << std::endl; // size of 1
    std::cout << std::bitset<8>( *ptr ) << std::endl; // 0000-0101
return 0;}

In this case constructor is a must have, since computer doesn't know how you want to structure the data. Sure in our logic, if we line up bits it is logical that assigning them would be same as they are sharing the memory. But the issue is computer will not do bitwise operators for us. Sure those bits are in order and lined up naturally ,but the computer just grabs some bit and define it as an unique variable, what you choose to place in that variable it is up to you.

If we were to exceed the scope of unit memory size (byte), OS starts interfering in our work.

#include <iostream> 
#include <bitset> 

struct temp3{
    bool b0:1;
    bool b1:1;
    bool b2:1;
    bool b3:1;
    bool b4:1;
    bool b5:1;
    bool b6:1;
    bool b7:1;

    temp3( int a ){
        this->b0 = ( a & 0x1 );
        this->b1 = ( a & 0x2 );
        this->b2 = ( a & 0x4 );
        this->b3 = ( a & 0x8 );
        this->b4 = ( a & 0x10 );
        this->b5 = ( a & 0x20 );
        this->b6 = ( a & 0x40 );
        this->b7 = ( a & 0x80 );
    }

};

int main(){
    struct temp3 t1 = 0xc3;
    uint8_t *ptr = (uint8_t *)&t1;
    std::cout << sizeof(struct temp3) << std::endl; // still size of 1
    std::cout << std::bitset<8>( *ptr ) << std::endl; // 1100-0011
return 0;}  

And when we exceed the byte size:

#include <iostream> 
#include <bitset> 
struct temp4{
    bool b0:1;
    bool b1:1;
    bool b2:1;
    bool b3:1;
    bool b4:1;
    bool b5:1;
    bool b6:1;
    bool b7:1;
    bool b8:1;

    temp4( int a ){
        this->b0 = ( a & 0x1 );
        this->b1 = ( a & 0x2 );
        this->b2 = ( a & 0x4 );
        this->b3 = ( a & 0x8 );
        this->b4 = ( a & 0x10 );
        this->b5 = ( a & 0x20 );
        this->b6 = ( a & 0x40 );
        this->b7 = ( a & 0x80 );
        this->b8 = ( a & 0x100 );
    }

};

int main(){
    struct temp4 t1 = 0x1c3;
    uint16_t *ptr = (uint16_t *)&t1;
    std::cout << sizeof(struct temp4) << std::endl; // size of 2
    std::cout << std::bitset<16>( *ptr ) << std::endl; // 0000-0000 1100-0011
    std::cout << t1.b8 << std::endl; // still returns 1
    std::cout << "\n\n";

    union t_as{
        uint16_t space;
        temp4 data;
        uint8_t bytes[2];
    };

    union t_as t2 = {0x1c3};
    //11000011-00000001
    std::cout << std::bitset<8>( t2.bytes[0] ) << "-"  << std::bitset<8>( t2.bytes[1] ) << std::endl;
return 0;}  

What happened here? Since we added another bool bit-field our struct grew for 1 byte ( since bool is 1 byte ), and our 16 bit pointer doesn't show the last b8 - but the union does. The issue is that OS took over, and in this case stuck the last bit behind our original memory - due to innate OS endianness. As you can see in the union , the byte is still read, but the order is different.

So when exceeding the byte size, normal OS rules apply.

CONCLUSION and ANSWER

struct half_opacity{
    uint8_t alpha:4;
    uint8_t red;
    uint8_t green;
    uint8_t blue;
    half_opacity(int a){
        this->alpha = ( a >> 24 )&0xf;
        this->red   = ( a >> 16 )&0xff;
        this->green = ( a >> 8  )&0xff;
        this->blue  = ( a & 0xff );
    }
    operator uint32_t(){
        return      ( this->alpha << 24 )
                |   ( this->red   << 16 )
                |   ( this->green << 8 )
                |   this->blue;
    }
};

{
    struct half_opacity c_space = 0xff00AABB;
    std::cout << "size of : " << sizeof(struct half_opacity) << std::endl; //size of : 4
    std::cout << std::hex << (uint32_t)c_space << std::endl; // 0x0f00AABB  
}

So unless you plan to confide the original channel to some bit size, i would strongly suggest using union approach, since there isn't any added benefit into splitting the 32 bit integer into individual bytes with bit-fields. The major thing about bit fields is that you need to split them and build then back up as with any other integer field - bit shifts often circumvent the whole OS endianness thing.

The truncation warning you got, was due to multiple members in your struct, and struct naturally assigning the first one , and since you added more than the bit field could handle the compiler warned you that some data will be lost.

Danilo
  • 1,017
  • 13
  • 32