1

I was toying around with a class where I wanted to index into it with an operator[], while also being able to access the fields.

I've attached an MCVE below of what I'm trying to do, which is be able to access a member variable through the variable itself, but also with some pointer offsets (ex: if there's an a, and b, then I can access b by the name, or access it by &a + 1 if they're the same type and located sequentially without padding).

I'm worried that I'll be running into undefined behavior and won't know it. Originally I was trying to do a "union with 1) members that are floats, and 2) array of floats" but I found out that it's undefined behavior. I tried looking up in the standard if what I'm about to do below is undefined behavior but wasn't able to find it (which obviously does not mean it doesn't exist, I easily could have missed it).

Since I'm also using CRTP to do this, I figure since I'm casting to itself that it should be okay as long as inheritance doesn't provide any members.

To make sure that this is possibly legal in C++, I added a bunch of static asserts which:

  • Make sure it's a standard layout, so I can use offsetof for other static asserts static_assert(std::is_standard_layout_v<Color>);
  • Make sure it's trivial static_assert(std::is_trivial_v<Color>);
  • Make sure the offsets are sequential static_assert(offsetof(Color, r) == 0);, static_assert(offsetof(Color, g) == sizeof(float));, static_assert(offsetof(Color, b) == 2 * sizeof(float));
  • Make sure nothing was added to the class from inheriting static_assert(sizeof(Color) == 3 * sizeof(float));

The code:

#include <iostream>

using namespace std;

template <typename T>
class ColorCRTP {
    T& getInstance() {
        return *static_cast<T*>(this);
    }

public:
    // Is it UB to do this when we set values from the
    // fields themselves in the actual class?
    float& operator[](size_t index) {
        // Assume the inheriting class *always* is only a
        // series of sequential members of the exact same
        // type.
        return *(&getInstance().r + index);
    }
};

struct Color : ColorCRTP<Color> {
    float r;
    float g;
    float b;

    Color() = default;
    Color(float r, float g, float b) : r(r), g(g), b(b) { }
};

// Do these help guarantee that I am not performing UB?
static_assert(std::is_standard_layout_v<Color>);
static_assert(std::is_trivial_v<Color>);
static_assert(offsetof(Color, r) == 0);
static_assert(offsetof(Color, g) == sizeof(float));
static_assert(offsetof(Color, b) == 2 * sizeof(float));
static_assert(sizeof(Color) == 3 * sizeof(float));

int main() {
    Color c{0.5f, 0.75f, 1.0f};

    c.g = 0.123f;        
    cout << c[1] << " = " << c.g << endl;

    c[1] = 0.321f; // This is legal or UB?
    cout << c[1] << " = " << c.g << endl;
}

Am I violating the standard and invoking undefined behavior by doing the above? Assuming no out-of-range indices are provided of course.

Since r is the first member, I don't know if 6.7.2 part 4.3 gives me further comfort in the fact that I'm referencing the first member in a safe way or not.

Water
  • 3,245
  • 3
  • 28
  • 58
  • *"// Do these help guarantee that I am not performing UB?"* No. Guaranteeing that the members are where you expect them doesn't eliminate the fact that you have undefined behavior. Undefined behavior can have roots in things other than memory layout, notably optimizations. If the compiler can ever determine that a certain branch contains undefined behavior, the observed behavior may not be what you expect. And that's just one scenario. Undefined behavior can do anything for any reason. It's not practical to try to constrain it's outcome in a portable way. – François Andrieux Mar 18 '19 at 15:56

1 Answers1

1

The behaviour of your program is undefined.

Pointer arithmetic is only valid within arrays. And r, g, b do not form an array.

Your best bet is to recode float& operator[](size_t) with a switch block comprising 3 labels.

Bathsheba
  • 231,907
  • 34
  • 361
  • 483
  • blast, beat my hammer by 14 seconds ;) – NathanOliver Mar 18 '19 at 15:53
  • @NathanOliver: Disk is cheap. This one is for `float`, the "dupe" is not exact enough for me. It's nice to have an answer fine-tuned to the question. – Bathsheba Mar 18 '19 at 15:53
  • 3
    @Bathsheba: Marking a question as a duplicate is not about sparing disk space. It's about not splitting information. If the answer exists elsewhere (and it does. `float` and `double` are not meaningfully different), then that's where this question should point. – Nicol Bolas Mar 18 '19 at 15:56
  • @NicolBolas: When I research a topic in a library I like to look at more than one book. We wouldn't remove Mark, Luke, and John from the Bible simply because we have Matthew. Yes, if you study the linked page you have a pretty good idea what the answer is, but then equally we could create a canonical C++ standards question and link all questions to that. Clearly that limit is absurd, but where do you draw the line? Again, disk is cheap - the idea of consolidating pages to duplicates is, in my opinion, quixotically futile, and in many ways damaging to the workings of the site. – Bathsheba Mar 18 '19 at 16:00
  • @Bathsheba: You can post that "book" as an answer to the other question. And let's face facts; your answer isn't very good; it says nothing the other answers don't, nor does it say it in a better way. Some of the answers on the duplicate cite the C++ standard on the subject. That makes it the canonical post on the matter. The only way dupe-closes "damage the site" is by preventing people from gaining extra rep from answering the same question over and over again. – Nicol Bolas Mar 18 '19 at 16:18
  • @NicolBolas: If you don't like my answer then the correct thing to do is downvote it. The receiving of a downvote is never to be taken personally (that is directed at other readers as I'm confident you share that opinion). – Bathsheba Mar 18 '19 at 16:24
  • @Bathsheba The goal is that by using duplicate links all of those "books" would refer to a single complete tome where all of the information is gathered. The ultimate goal is to avoid having to search through several questions to find the best answers. See [this article](https://stackoverflow.blog/2010/11/16/dr-strangedupe-or-how-i-learned-to-stop-worrying-and-love-duplication/) linked to by the [Help Center's page on duplicate](https://stackoverflow.com/help/duplicates). If you feel this policy is damaging to the site you can make a post on https://meta.stackexchange.com/ about it. – François Andrieux Mar 18 '19 at 16:40
  • @FrançoisAndrieux: We should let the community decide. My understanding is that if this is downvoted to net 0 or negative, then the whole page is gobbled up by a bot? – Bathsheba Mar 18 '19 at 16:49
  • @Bathsheba Policy is mostly determined by the community. Currently, most users agree to close duplicates to consolidate answers. If this is a problem or you feel a large user base disagrees with this, you can post on meta to try to have the policy changes. In the mean time, it's more helpful to conform to the current guidelines. As far as I know answers don't get automatically deleted. Question *do* get deleted according to [these guidelines](https://meta.stackexchange.com/help/roomba). This feature is sometimes called the Roomba. Notice duplicates are never Roomba'ed. – François Andrieux Mar 18 '19 at 17:00
  • 1
    I like this answer because it gives me a valid defined behavior and simple solution to the problem that the other related post does not have. Also to the discussion above: I always look for duplicates and search this place for my issues before posting, and the related post never showed up in any searches. I spend a large amount of time searching the internet/SO before posting and the related post never came up. It'd be nice if there was a way to tag metadata with questions, as that would have saved me a bunch of time writing my post up. I am fortunate enough to get an answer I'm happy with. – Water Mar 18 '19 at 17:39