13

Let's consider this example code:

struct sso
{
    union {
        struct {
            char* ptr;
            char size_r[8];
        } large_str;
        char short_str[16];
    };

    const char* get_tag_ptr() const {
        return short_str+15;
    }
};

In [basic.expr] it is specified that pointer arithmetic is allowed as long as the result points to another element of the array (or past the end of an object or of the last element). Nevertheless it is not specified in this setion what happens if the array is an inactive member of a union. I believe it is not an issue short_str+15 is never UB. Is it right?

The following question clearly showes my intent

Oliv
  • 17,610
  • 1
  • 29
  • 72
  • 3
    IIRC it's not UB until you actually try to dereference the resulting pointer. – Some programmer dude Jan 10 '18 at 13:42
  • 2
    @Someprogrammerdude No, pointer arithmetic itself can produce undefined behavior... see, for instance, the special-casing of the one-after-the-end pointer (which you can calculate but cannot dereference). Of course, it's the sort of pedantic UB which will never get you into trouble, but this question *is* tagged "language-lawyer". – Sneftel Jan 10 '18 at 13:47
  • 1
    But, on that basis you are saying that a pointer taken when the member was active becomes UB when inactive (which I can live with) and stays UB when back into active scope? To be honest, I find the whole idea that the compiler might optimise a union as anything other than a single unit worrisome. – Gem Taylor Jan 10 '18 at 14:00
  • You should use `std::variant` instead of raw unions. – Dmitry Sazonov Jan 10 '18 at 14:04
  • @GemTaylor I was speaking in generalities, not specifically with respect to unions. Remember, though, that UB is about *behavior*, not *values*. Dereferencing a pointer to a valid object is fine, regardless of whether dereferencing it at some other point would have produced UB. – Sneftel Jan 10 '18 at 14:05
  • @DmitrySazanov Read the fowing question, my intention is not to have memory associated to a tag as is done by `std::any`. I was wondering if facebook::string could be implemented without UB and without implementation defined behavior. – Oliv Jan 10 '18 at 14:06
  • @Sneftel Yes, but the concept of active and lifetime for anything else is (fairly) easy to understand, as it matches scope. Only unions have this concept of sub active states AFAICT. From the POV of anything else, it is OK (not good, but) to have a pointer to something after deleting it, even copy it, if you don't dereference it. It is intuitively /not/ OK to do pointer math on that stale pointer, though it is harmless in most (all) implementations. It would be valid for some interpreter to say "I can't see the object any more, so I can't let you do that pointer math". And so we get to union. – Gem Taylor Jan 10 '18 at 15:30
  • Accessing an inactive union member is UB in C++. Also see [Accessing inactive union member and undefined behavior?](https://stackoverflow.com/q/11373203/608639) You also got an anonymous union, and I think that's UB too in C++. Also see [Why does C++ disallow anonymous structs?](https://stackoverflow.com/q/2253878/608639) In the end, it looks like a lot of C code pigeon-holed into C++. – jww Jan 10 '18 at 16:31
  • @jww There are no union member *object* access here. See [\[intro.defs\]/access](https://timsong-cpp.github.io/cppwp/n4659/intro.defs#defns.access). Actualy you are confusing [expr.ref] which is called "class member access". And the access to the object value which is constrained in [basic.life] – Oliv Jan 10 '18 at 16:43

2 Answers2

6

Writing return short_str+15;, you take the address of an object whose lifetime may have not started, but this does not result in undefined behavior unless you dereference it.

[basic.life]/1.2

if the object is a union member or subobject thereof, its lifetime only begins if that union member is the initialized member in the union, or as described in [class.union].

and

[class.union]/1

In a union, a non-static data member is active if its name refers to an object whose lifetime has begun and has not ended ([basic.life]). At most one of the non-static data members of an object of union type can be active at any time, that is, the value of at most one of the non-static data members can be stored in a union at any time.

but

[basic.life]/6

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.allocation]), and using the pointer as if the pointer were of type void* , is well-defined. Indirection through such a pointer is permitted but the resulting lvalue may only be used in limited ways, as described below.
- [list unrelated to unions]

YSC
  • 38,212
  • 9
  • 96
  • 149
  • 1
    I concur with all that. Might also want to mention from class.union "Each non-static data member is allocated as if it were the sole member of a struct", meaning that all the union members' storages are allocated even when they are not active. – Sneftel Jan 10 '18 at 14:08
  • So maybe you want to answer this [related question](https://stackoverflow.com/questions/48189026/using-stdlaunder-to-get-a-pointer-to-an-active-object-member-from-a-pointer-to) – Oliv Jan 10 '18 at 14:13
  • 2
    Are you sure `short_str+15` does "use the pointer as if the pointer were of type `void*`"? – xskxzr Jan 10 '18 at 14:14
  • The result of `short_str+15` depends on the type of `short_str`, so I don't think it is used "as if the pointer were of type `void*`". – xskxzr Jan 10 '18 at 14:30
  • @xskxzr After some time to think twice, `short_str+15` depends on the _static_ type of `short_str`, which is a known compilation-time information and is not impacted by run-time concept like the lifetime of the object it is pointing to. – YSC Apr 12 '18 at 06:10
3

Whether pointer arithmetic on union members will lead to aliasing depends upon how the pointers will end up being used. On implementations which supplement the Standard with a guarantee that "type-access" rules will only be applied in cases where there is actual aliasing, or (for C++) in cases involving types with non-trivial semantics, the validity of pointer operations would have little to do with whether they are performed upon active or inactive members.

Consider, for example:

#include <stdint.h>

uint32_t readU(uint32_t *p) { return *p; }
void writeD(double *p, double v) { *p = v; }

union udBlob { double dd[2]; uint32_t ww[4]; } udb;

uint32_t noAliasing(int i, int j)
{
  if (readU(udb.ww+i))
    writeD(udb.dd+j, 1.0);
  return readU(udb.ww+i);
}

uint32_t aliasesUnlessDisjoint(int i, int j)
{
  uint32_t *up = udb.ww+i;
  double *dp = udb.dd+j;

  if (readU(up))
    writeD(dp, 1.0);
  return readU(up);
}

During the execution of readU, no storage that is accessed via *p will be accessed via any other means, so there is no aliasing during the execution of that function. Likewise during the execution of writeD. During the execution of noAliasing, all operations that will affect any storage associated with udb are performed using pointers that are all derived from udb and clearly have active lifetimes that clearly do not overlap, so there is no aliasing there.

During the execution of aliasesUnlessDisjoint, all accesses are performed using pointers which are derived from udb, but storage is accessed via up between the creation and use of dp, and storage is accessed via dp between the creation and use of up. Consequently, *dp and *up will alias during the execution of aliasesUnlessDisjoint unless udb.ww[i] and udb.dd[j] occupy disjoint storage.

Note that both gcc and clang apply type-access rules even in cases like the no-aliasing function above where there is no actual aliasing. Despite the fact that the Standard explicitly says that an lvalue expression of the form someArray[y] is equivalent to *(someArray+(y)), gcc and clang will only allow reliable access to array members within a union if the [] syntax is used. For example:

uint32_t noAliasing2(int i, int j)
{
  if (udb.ww[i])
    udb.ww[j] = 1.0;
  return udb.ww[i];
}
uint32_t noAliasing3(int i, int j)
{
  if (*(udb.ww+i))
    *(udb.dd+j) = 1.0;
  return *(udb.ww+i);
}

Although the code produced by gcc or clang for noAliasing2 will reload udb.ww[i] after the operation on udb.dd[j], the code for noAliasing3 will not. This is technically allowable under the Standard (since the rules, as written, don't allow udb.ww[i] to be accessed under any circumstances!), but that in no way implies any judgment on the part of the authors that the behavior of gcc and clang is appropriate in a high-quality implementations. Looking purely at the Standards, I see nothing to suggest that any particular one of the noAliasing forms should be more or less valid than any other, but programmers considering use of gcc or clang in -fstrict-aliasing mode should recognize that gcc and clang treat them differently.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • This is interesting, I did not take attention to that. To be fair with gcc, this behavior is documented ([gcc documentation](https://gcc.gnu.org/onlinedocs/gcc-8.2.0/gcc/Optimize-Options.html#Type-punning)) – Oliv Sep 14 '18 at 17:33
  • @Oliv: IMHO, the Standard should recognize a separate category of implementation for compilers like gcc and clang which require that any storage which is ever accessed using any particular non-character type never be written with any other type, nor read with any other non-character type, thus relieving gcc and clang of the burden of trying to comply with the unworkable corner cases of the Effective Type rules, but simultaneously recognizing the legitimacy of access patterns that gcc and clang can't support, but which other compilers can (even in `-fstrict-aliasing` mode). – supercat Sep 14 '18 at 18:31
  • @Oliv: I think the wheels fell off for gcc when some authors wrote examples of "potentially non-working" code that don't involve aliasing, even though supporting non-aliasing cases would have been trivial. This led to the adoption of intermediate representations which filter out the differences between all of the above except `noAliasing2`, and thus treat both other forms as equivalent to `aliasesUnlessDisjoint`. – supercat Sep 14 '18 at 18:42