9

In the comments to this answer, Koushik raised a very valid point.

Take the following:

union U
{
    int x;
    const T y;
};

(I choose T such that there is no common initial sequence of layout compatibility here, meaning only one member may be active at any given time per [C++11: 9.5/1].)

Since only one member may be "active" at any one time (made active by writing to it), and y cannot be written to after initialisation, isn't this rather pointless? I mean, y can only be read from until the first time x is written to, and at that only if y was the initialised member.

Is there some use case I'm missing? Or is this indeed a pretty pointless confluence of language features?

(This has been mentioned before)

Community
  • 1
  • 1
Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • @OMerObaid - The interesting bit is when there is a const member. – Dennis Jan 03 '14 at 14:30
  • 4
    @OMerObaid Your comment is the perfect example why we should be able to downvote comments into oblivion! – Praetorian Jan 03 '14 at 14:32
  • I've seen this in C where it was (ab?)used to perform a "const cast". – Kerrek SB Jan 03 '14 at 14:35
  • 1
    @Kerrek SB, I still have some C code that does this. New Year's Resolution: must refactor this. – Bathsheba Jan 03 '14 at 14:36
  • @Bathsheba: In C, it's hard to argue against this. If you want to write something like `strchr`, I don't think there's any decent combination of casts and compiler warning flags that's entirely satisfactory, i.e. doesn't warn iff the code is intentional... – Kerrek SB Jan 03 '14 at 14:41
  • Couldn't you do something like `T t = {42}; new (const_cast(&t.y)) const int{42};`? -- I'm not sure if that's UB or not. – dyp Jan 03 '14 at 14:47
  • @DyP: Looks pretty UB to me. – Lightness Races in Orbit Jan 03 '14 at 14:48
  • @LightnessRacesinOrbit I'm sure you can't do that twice, but can you do it once w/o UB? – dyp Jan 03 '14 at 14:52
  • @DyP: You're writing into a `const` field (kind of) post-initialisation – Lightness Races in Orbit Jan 03 '14 at 14:54
  • @RaymondChen: I'm certainly not disputing that; I guess I'm looking to confirm whether or not that is the case here, without any prejudice if it is. – Lightness Races in Orbit Jan 03 '14 at 15:03
  • (Sorry, I deleted the comment you're replying to. My original comment said basically "sometimes you can combine features in meaningless ways.") It may become meaningful in some future version of f C++ adopts named initialization of unions. Then you could write `T a = { .y = 3 };`. Note also that forbidding meaningless things creates extra work both for the compiler and the standards committee (who need to be absolutely sure that the construct is meaningless). – Raymond Chen Jan 03 '14 at 15:05
  • Note: your last edit seems incorrect, `union T` can't contain a `T` member (g++ `error: field 'y' has incomplete type`). – gx_ Jan 04 '14 at 17:41
  • @gx_: Dammit, typo! Thanks – Lightness Races in Orbit Jan 04 '14 at 17:43

4 Answers4

2

It does have uses:

1) For offering a const_cast-like technique. In a sense, x = const_cast<...>(y).

2) When dealing with templates, sometimes you need a const version of a data type so you match other parameter types.

(I've seen (1) used when programming against legacy interfaces).

Bathsheba
  • 231,907
  • 34
  • 361
  • 483
  • 1) ah yeah nice idea. – Brandon Jan 03 '14 at 14:35
  • 3
    1) How so? Are you proposing writing to `y` then reading from `x`? That is UB, you know. _Actually_ writing `const_cast` (or letting it happen implicitly) is the canonical way to add `const`ness. – Lightness Races in Orbit Jan 03 '14 at 14:45
  • @Bathsheba: Implementations don't "invoke UB". It's not some magical switch built-in to compilers that they flip when they hit some behaviour they don't like. UB is dictated by the standard, and it means that the implementation can do whatever it likes, including not bothering with consistency or even successful compilation. The issue is _precisely_ that you'd have to hypothecate about this; – Lightness Races in Orbit Jan 03 '14 at 15:00
  • _(cont.)_ you can make guesses using layout rules, but they are all assumptions _that even the implementation itself is no longer bound by in such a case_. Stick to the well-defined rules of the language and you'll come out on top. The argument "but everybody else does it a lot, therefore it's standard" does not make any logical sense. – Lightness Races in Orbit Jan 03 '14 at 15:01
  • 1
    @LightnessRacesinOrbit: That is not UB but rather useless. It is not undefined behavior as the layout of both members is the same and the standard allows reading from a different member than the active one if they are layout-compatible. Now, it is useless, since you are not *casting* const-ness out of an object, but making a copy, forcing the copy to be const and then reading through a different member... well, just copy directly into the `x`! :) – David Rodríguez - dribeas Jan 03 '14 at 15:04
  • @DavidRodríguez-dribeas: That's true, the common initial sequence rule helps out there. – Lightness Races in Orbit Jan 03 '14 at 15:06
2

Not using unions a lot, but this might be scenario:

#include <iostream>

class Accessor;
union Union
{
    private:
    friend class Accessor;
    int write;

    public:
    const int read;

    Union() : read(0) {}
};

class Accessor  {
    public:
    static void apply(Union& u, int i) { u.write = i; }
};

int main() {
    Union u;
    // error: ‘int Union::write’ is private
    // u.write = 1;
    std::cout << u.read << '\n';
    Accessor::apply(u, 1);
    std::cout << u.read << '\n';
}

Note: From 9.5 Unions

Note: One special guarantee is made in order to simplify the use of unions: If a standard-layout union contains several standard-layout structs that share a common initial sequence (9.2), and if an object of this standard-layout union type contains one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of standard-layout struct members; see 9.2. — end note ]

SirGuy
  • 10,660
  • 2
  • 36
  • 66
  • 3
    Again, this is **not** undefined behavior, `int` and `const int` are layout compatible --or at least I would hope so. And 9.5/1 guarantees this. – David Rodríguez - dribeas Jan 03 '14 at 15:06
  • Yes, true, as there is a common initial sequence. _At the very least_ I would expect a comment above the union declaration making clear that code using it will become UB if its members are changed. – Lightness Races in Orbit Jan 03 '14 at 15:06
  • @DavidRodríguez-dribeas Unfortunately, I can only find something about layout-compatibility on basic types *if they're the same type* in [basic.types]/11. And cv-qualified/unqualified versions of types are distinct types according to [basic.type.qualifier]/1 (where it's also specified that they have the same *representation and alignment*, if that matters) – dyp Jan 03 '14 at 15:25
  • @DyP: Yes, I went through that same process in the past and it is not obvious at all in the wording, but I can only imagine that `int` and `const int` must be layout compatible :) – David Rodríguez - dribeas Jan 03 '14 at 16:12
  • @DavidRodríguez-dribeas imagine a more complex type, where when the value is `const` the compiler can prove that a particular field is always `7`. Can it omit that field? – Yakk - Adam Nevraumont Jan 03 '14 at 16:57
  • @Yakk: No (I think). The standard guarantee that *cv-qualified or cv-unqualified versions of a type are distinct types; however, they shall have the same representation and alignment requirements* (3.9.3/1). Could it change the layout under the *as-if* rule? For that it would have to prove that the value is always 7, but also prove that in all other translation units it will be able to prove the same. From a practical point of view, the cases where that could be done are so limited, and the potential issues that it would raise so large that I doubt any compiler vendor would invest on that. – David Rodríguez - dribeas Jan 03 '14 at 17:20
2

Here's a contrived example of a reference-semantics type where you'd only want to grant const access to. The union is used in a variant-like data type returned from a "type-erasing" function.

#include <memory>

template<class T>
struct reference_semantics
{
public:
    reference_semantics(T* p ) : m(p) {}

    int observe() const { return *m; }
    void change(T p) { *m = p; }

private:
    T* m;
};

struct variant
{
    enum T { INT, DOUBLE } type;

    union U
    {
        reference_semantics<int> const i;
        reference_semantics<double> const d;

        U(int* p) : i(p) {}
        U(double* p) : d(p) {}
    } u;
};

#include <iostream>
std::ostream& operator<<(std::ostream& o, variant const& v)
{
    switch(v.type)
    {
        case variant::INT:
            return o << "INT: "<<v.u.i.observe();
        case variant::DOUBLE:
            return o << "DOUBLE: "<<v.u.d.observe();
    }
}

#include <string>

variant type_erased_access(std::string name)
{
    // imagine accesses to a map or so

    static double dval = 42.21;
    static int ival = 1729;

    if(name == "Lightness") return { variant::DOUBLE, &dval };
    else return { variant::INT, &ival };
}

int main()
{
    variant v0( type_erased_access("Lightness") );
    std::cout << v0 << "\n";
    variant v1( type_erased_access("Darkness") );
    std::cout << v1 << "\n";
}

Imagine now that instead of int and double, much larger data types are used, and that the reference_semantics data type actually provides more functionality than just returning the value.

It might even be possible that you want to return a reference_semantics<some_type> const for some arguments, but a plain int for others. In that case, your union might even have const and non-const members.

dyp
  • 38,334
  • 13
  • 112
  • 177
-1

If the union represents part of a result of some method/algorithm, then it could make sense. But in that case, I'd make both values const:

union T
{
    const int x;
    const int y;
};
Brandon
  • 38,310
  • 8
  • 82
  • 87