Home > Software engineering >  In C , is it valid to treat scalar members of a struct as if they comprised an array?
In C , is it valid to treat scalar members of a struct as if they comprised an array?

Time:09-30

While looking at the code for Dear Imgui, I found the following code (edited for relevance):

struct ImVec2
{
    float x, y;
    float& operator[] (size_t idx) { return (&x)[idx]; }
};

It's pretty clear that this works in practice, but from the perspective of the C standard, is this code legal? And if not, do any of the major compilers (G , MSVC, Clang) offer any explicit or implicit guarantees that this code will work as intended?

CodePudding user response:

is this code legal?

No, it has undefined behavior. The expression &x is a float* that points to a float object and not to the first element of a float array. So, in case idx is 1 or 2 or some other value, the expression (&x)[idx] is (&x)[1] or (&x)[2] respectively which means you're trying to access memory that is not meant to be accessed by you.

do any of the major compilers (G , MSVC, Clang) offer any explicit or implicit guarantees that this code will work as intended?

Undefined behavior means anything1 can happen including but not limited to the program giving your expected output. But never rely(or make conclusions based) on the output of a program that has undefined behavior. The program may just crash.

So the output that you're seeing(maybe seeing) is a result of undefined behavior. And as i said don't rely on the output of a program that has UB. The program may just crash.

So the first step to make the program correct would be to remove UB. Then and only then you can start reasoning about the output of the program.


1For a more technically accurate definition of undefined behavior see this, where it is mentioned that: there are no restrictions on the behavior of the program.

CodePudding user response:

The reality is that the type punning solution has been used successfully in C for ages. The problem is that it is fragile, and that C is not C — additional problems arise that you may not account for.

For a solution you might find palatable, I suggest reference accessors:

#include <iostream>
#include <stdexcept>

struct point
{
  double xy[2];

  double & x() { return xy[0]; }
  double & y() { return xy[1]; }

  double const & x() const { return xy[0]; }
  double const & y() const { return xy[1]; }

  double       & operator [] ( std::size_t n )       { return xy[n]; }
  double const & operator [] ( std::size_t n ) const { return xy[n]; }
};

int main()
{
  point p{ 2, 3 };
  
  std::cout << p[0] << ", " << p.x() << "\n";
  std::cout << p[1] << ", " << p.y() << "\n";
  
  p[0]  = 5;
  p.y() = 7;
  std::cout << p[0] << ", " << p.x() << "\n";
  std::cout << p[1] << ", " << p.y() << "\n";
  
  auto f = []( const point & p )
  {
#if 0
    p[0]  = 11;  // won't compile
    p.y() = 13;  // won't compile
#endif
    std::cout << p[0] << ", " << p.x() << "\n";
    std::cout << p[1] << ", " << p.y() << "\n";
  };
  f( p );
}

That compiles very cleanly.

You might be tempted to just use references directly:

struct point
{
  double xy[2];
  double & x;  // DON’T DO THIS
  double & y;  // DON’T DO THIS

  point() : x{xy[0]}, y{xy[0]} { }
  point( double x, double y ) : x{xy[0]=x}, y{xy[1]=y} { }
};

The problem with this last approach is that it breaks const guarantees. That is, even if you have a const point somewhere, you could still modify it through the references.

void f( const point & p )
{
  p[0] = 97;          // compiler complains properly
  p.y  = 3.14159265;  // compiler blithely accepts this
}

Hence, use reference accessor methods.

  • Related