5

I've found a few threads that heavily imply this can't be done, but none use exactly the same combination of operators and conditions, so I'd like to ask more specifically. Hopefully that means it's a quick and easy answer for someone... one way or another!

Consider an example proxy class, made to manage a value within a larger block of storage - as in this oversimplified but representative example:

class SomeProxyThing {
    std::uint32_t storage;

public:
    operator std::uint16_t() const
    {
        return storage & 0x0000FFFF;
    }

    SomeProxyThing &operator=(std::uint16_t const value)
    {
        storage &= 0xFFFF0000;
        storage |= value;
    }
};

I want all assignments to work via the user-defined operators. The user should only be able to pass in or get out the 'exposed' type, in this case std::uint16_t. I might be using various proxy class types and want this to apply to all of them. Ideally, for any combination of types, I could just type someProxy = anotherProxy and let the compiler do the rest.

But when the left- and right-hand-side of the assignment have the same or inheritance-related types, the default copy assignment operator - of course - conflicts with this goal. It copies the entire storage, thus clobbering the other half of that uint32_t - rather than copying just the 'exposed' value as desired. And rightly so! For most cases. But I'd like a way to 'assign by conversion' even if LHS and RHS types are the same. To avoid this, I can:

  • redefine the copy assignment operator to perform a 'proxied' copy using the user-defined operators - which is what I've been doing, but it seems kinda hacky and, like any user-defined constructor/assignment operator, breaks the trivially copyable status of the struct - which I need to keep. It still memcpy()s anyway in g++, but I want defined behaviour.
  • or = delete the copy-assignment operator (which we can now do for TC types). But assignments still try to use it and throw a compile error - since delete means 'abort with an error if I'm the chosen overload', not 'exclude me from overload resolution'. To get around this, I must explicitly tell the compiler to use the conversion operator and assign from its result:
SomeProxyThing a, b;
a = 42;
b = static_cast<std::uint16_t>(a);
// a.k.a.
b.operator=( a.operator std::uint16_t() );

There doesn't seem to be a way to tell the compiler 'ignore any error generated by your preferred overload and pick the next best one'. Is there? More generally, is there any way/hack/horrifying kludge, in such a situation, to force the compiler to automatically use/prefer certain operators?

In other words, ideally, in

SomeProxyThing a, b;
a = 42;
b = a;

that b = a; would really do this:

b = static_cast<std::uint16_t>(a);
// a.k.a.
b.operator=( a.operator std::uint16_t() );

without me having to type this manually, use a static_cast, or implement named get/set methods. Ideally, I want reads/writes to any such proxy to look exactly like reads/writes to basic types in written code, all using =.

I strongly suspect that's not possible... but confirmation would be nice!

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
underscore_d
  • 6,309
  • 3
  • 38
  • 64
  • 4
    There seems to be a contradiction in your requirements: when a copy is to be made, you don't want the behaviour of the default copy assignment operator, but you still want the class to be trivially copyable. You can't have both; trivially copyable implies (among other things) that the default operations are the correct ones for the class. – bogdan Jul 12 '16 at 12:25
  • @bogdan Of course. I'm aware it's rather paradoxical that I want assignment from an identical type to assign via a conversion operator, so I doubt it's possible - just wanting confirmation really. In my current project, I use several variations on this theme, which all assign correctly to _each other_ via conversion operators - it's only when the LHS and RHS have the same (or derived) type that this issue gets in the way. I really just want to be lazy and use the same syntax for all combinations, but my gut feeling is that it can't be done. – underscore_d Jul 12 '16 at 12:28
  • `memcpy`ability is my real goal, not trivial copy-assignment, but the Standard considers them intrinsically related, for obvious reasons. There is [a thread discussing potential refinements to _trivially copyable_ and proposing a new category of "memcpy"-capable types](https://groups.google.com/a/isocpp.org/forum/#!topic/std-discussion/wphImiqfX7Y) that might add more nuance, but I'm not aware of that having gone anywhere practically. – underscore_d Jul 12 '16 at 12:35
  • 1
    In other words, you want your class to be trivially copy and move constructible (and destructible, I guess), but not trivially copy and move assignable. That makes sense, strange as it is, but it also means that your class can never be trivially copyable in the Standard sense, if you want these all at once. One crazy thought would be not to require them all at once. Have the main class be trivially copyable with deleted assignments and, when you want convenient assignments, use a wrapper that provides the non-trivial operators you want. The wrapper would need to be *very* carefully written... – bogdan Jul 12 '16 at 15:01
  • @bogdan Phew, I'm glad to hear it makes some kind of sense :-) Yeah, it's not a valid category unless the Standard adds some intermediate classification - for which I shan't hold my breath! The idea of a wrapper is good and something I vaguely pondered earlier, but I think you've phrased it in a better way, which might give me a better idea of how to explore it. I'll see how it goes but guess it's not worth frying my brain _too_ much just to be able to write `a = b` for all combinations: ultimately, I can achieve the same thing with a bit more manual typing. It'd just be nice not to need that! – underscore_d Jul 12 '16 at 15:08
  • When you define an assignment operator you don't get a copy assignment operator. It's that simple. – Cheers and hth. - Alf Jul 12 '16 at 15:30
  • @Cheersandhth.-Alf Yes, I'm very aware they're separate entities, hence this whole question. I'm assuming your comment intends to confirm my suspicion that there's no way to have `lhs = rhs` prefer the normal assignment operator if both sides are the same or related types, without manually implementing copy-assignment in terms of normal assignment and thus (and again, quite fairly) losing trivially copyable status. – underscore_d Jul 12 '16 at 16:27
  • 1
    So you expect `SomeProxy a; a = b;` work differently than `SomeProxy a = b;`? That seems to be very dangerous and controversial. I mean it will produce endless hard to catch bugs in your code. – Slava Jul 12 '16 at 19:40
  • @Slava I'm aware of the pitfalls. I don't want that specifically: what I want is (A) Standard-compliant `memcpy`ability _but_ (B) an assignment `operator=()` that always assigns from a nominated conversion type on RHS, coercing RHS to fit that, **even if RHS is the same type**. It seems this just isn't expressible in C++ because the default copy-assignment (rightly!) supersedes it, and the only workaround is to make _it_ do a 'proxied' write to `storage`... which means we're not trivially copyable, so `memcpy` is _presumably_ UB. I wish standard-layout were `memcpy`able, not sure why it isn't? – underscore_d Jul 12 '16 at 19:49
  • 1
    @underscore_d you may not want that but that follows from your requirements "trivially copyable" and at the same time "non trivial assignment" when you assign objects of the same type. You should make assignment explicit, but you are spending your time to be able to write terrible code (in support point of view). Somebody else or even you after couple month look into code "SomeProxyThing a; a = b;` and say "why I do not copy initialize it? Let's fix it". – Slava Jul 12 '16 at 19:59
  • @Slava I'm asking as an educational exercise, not as a way to allow myself to write terrible code. In fact, if I wanted to write terrible code, rather than asking this question, wouldn't I just keep coding a user-defined copy-assignment operator & then `memcpy`ing, despite the resulting object not being trivially copyable & hence this process being UB-ish...? I'm certainly going to write the assignment explicitly in real code; this thread is more to confirm there's no other way, through the very valid points of other programmers like yourself, & maybe to learn some other good things on the way – underscore_d Jul 12 '16 at 20:02
  • @underscore_d I do not quite understand what educational exercise is to have copy initialization and assignment work differently. What value that produces? How it is possible to write not terrible code with such concept? – Slava Jul 12 '16 at 20:07
  • @Slava Read it again: the educational exercise is _asking the question whether this is possible/why isn't it_, not trying to find a way around the ensuing valid objections to it. By asking, it's become clearer to me that something I originally thought 'didn't make much sense' is actually a total paradox. How does that not count as educational? – underscore_d Jul 12 '16 at 20:08
  • @underscore_d yes that is does not make sense is educational. And I gave you that answer, but you are arguing with it. What I do not see is value over "this does not make sense but how can I do that?". You can shoot your leg in C++ many ways, do you want to learn them all? – Slava Jul 12 '16 at 20:17
  • @Slava Show me where I'm arguing with the assertion that it doesn't make sense. I'll wait. Was it perhaps when I completely agreed with you by saying "I'm certainly going to write the assignment explicitly in real code"? – underscore_d Jul 12 '16 at 20:21
  • @underscore_d duh, your requirements "object is trivial-copyable and assignment operator for the same type is custom" is equivalent to "statements `Foo a = b;` and `Foo a; a = b;` will produce different effect" even if you say you do not want it. So it does not really matter if it is possible or not, it should not be. You do not need to dig deeper to find a way to screw your code up. – Slava Jul 12 '16 at 20:30
  • @Slava And I'm not, **anymore**. That's the point. _That's_ what's been educational. You're calling me out now for something I said in the past that I've now recanted, having realised that it was a bad idea. Why bother? I am not digging deeper to find a way to screw my code up. This question existed to determine whether or not there was a way to do this _without_ screwing my code up, and it seems that everyone has indicated 'no', so I'm just going to do things the verbose-but-safe way. Do you get it yet? – underscore_d Jul 12 '16 at 20:33
  • @underscore_d ok we agreed, I just may not understand it :) – Slava Jul 12 '16 at 20:39
  • @Slava Phew! Thanks for making me think so hard about it. :D +1ing your initial comments as they're very clear statements of the problem with this idea. – underscore_d Jul 12 '16 at 20:43

2 Answers2

1

You can do this:

#include <stdint.h>
#include <iostream>
#include <type_traits>

using namespace std;

class Proxy_state
{
protected:
    uint32_t storage;
public:
    // Access to the bytes
};

static_assert( is_trivially_copyable<Proxy_state>::value, "!" );

class Some_proxy_thing
    : public Proxy_state
{
private:

public:
    operator std::uint16_t() const
    {
        return storage & 0x0000FFFF;
    }

    auto operator=( uint16_t const value )
        -> Some_proxy_thing&
    {
        clog << "=(uint16_t)" << endl;
        storage &= 0xFFFF0000;
        storage |= value;
        return *this;
    }

    auto operator=( Some_proxy_thing const& value )
        -> Some_proxy_thing&
    { return operator=( static_cast<uint16_t>( value ) ); }
};

static_assert( not is_trivially_copyable<Some_proxy_thing>::value, "!" );

auto main()
    -> int
{
    Some_proxy_thing    a{};
    Some_proxy_thing    b{};
    const Some_proxy_thing c = b;

    a = c;

    a = 123;
    a = b;
}

Here all three assignments output (to the standard error stream) =(uint16t).

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • You still get the implicitly declared copy assignment; it just loses to the template in overload resolution. `const Some_proxy_thing& c = b; a = c;` still won't work. – T.C. Jul 12 '16 at 19:28
  • Thanks Alf, interesting idea - I had wondered whether templates might help but didn't think of any ways they could. @T.C. is correct, although I could mostly live without being able to assign-from-`const` as I don't tend to use `const` versions of such objects. However, more to the point - doesn't the `template` really just generate a user-provided move assignment operator for `SomeProxyThing &&`, thus breaking trivial copyability again? All signs are pointing towards this just not being possible, though I'd love to be proven wrong in thinking that! – underscore_d Jul 12 '16 at 19:36
  • @T.C. (and underscore_d): Thanks, fixed that. I think this is about as good as possible. Otherwise I think you'd have to change the requirements. – Cheers and hth. - Alf Jul 12 '16 at 19:46
  • @underscore_d: In the original code I posted, with a template, the reference was not an rvalue reference as such. It just looked like one. It was a [universal reference](https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers). – Cheers and hth. - Alf Jul 12 '16 at 19:50
  • The code now is exactly what I already had, before I realised that the user-provided copy-assignment operator breaks trivial copyability. It's allowed for standard-layout types, which I'd previously assumed were `memcpy`able, but they're not Standardised as such... and I'm not sure why? If you've got a reason I'm overlooking, then that'll save me asking another daft question another day. ;-) Anyway, it's looking like I'll just have to accept that my ideal is a paradox & that I'll have to be `static_cast`ing whenever I coincidentally end up having to assign between related types. Thanks anyway! – underscore_d Jul 12 '16 at 19:53
  • 1
    @underscore_d: Look closer. There's inheritance, providing access to the trivially copyable state. Or you can make it an ordinary data member plus maybe a `copy_to_bytes` function or such. – Cheers and hth. - Alf Jul 12 '16 at 19:54
  • @Cheersandhth.-Alf Whoops, that's what I get for not scrolling up. Nice idea, and this might prove very useful in other situations, though at present I think I need each object to be self-contained and trivially copyable as-is... I'll ponder further, though. – underscore_d Jul 12 '16 at 19:57
  • @Cheersandhth.-Alf For the original, `template`d version, can you elaborate on what the `&&` being a universal reference means in practical terms? i.e. if it doesn't amount to being a move-assignment operator when called with `SomeProxyType &&` The distinction between rvalue and universal references is something I don't quite get, and I can't fathom what its effects here are. It's possible that, although as T.C. said it doesn't cover all situations, it could still serve as a nice way to get the desired shorthand when assigning from non-`const`. But only if it doesn't qualify as move assignment – underscore_d Jul 12 '16 at 20:45
  • 1
    Oh. I linked to Scott Meyers' exposition, and I think he invented the term. But anyway, due to the rules for template type inference the upshot is that a `T&&` function argument matched with an lvalue argument, becomes a `T&` (possibly with cv-qualification), and matched with rvalue argument it becomes `T&&`, rvalue reference. So for a call with rvalue argument, the templated `operator=` reduced to one with ralue reference formal argument. However, I'm pretty sure such instantiation is not considered a move assignment operator. Weasel words because std keeps changing, but the compilers agreed. – Cheers and hth. - Alf Jul 12 '16 at 21:14
  • @Cheersandhth.-Alf Ha, the link was almost imperceptibly dark blue, which must mean I've _tried_ to read it before! It works with rvalues + if called with lvalue deduces `SomeProxyThing &` (which we can't declare for TC) + works for non-`const` RHS as T.C. said. Oddly/unsurprisingly(?) it seems to block the default move constructor: if I call `b = std::move(a)`, I'm informed by `__PRETTY_FUNCTION__` that `T = Proxy` i.e. value assignment. :S Well, _assuming_ trivial copyability is maintained as `g++` says, thanks for the `template` trick to assign from non-`const`. Wrapping isn't usable though – underscore_d Jul 13 '16 at 12:46
  • BTW, `auto main() -> int` - what a crap? – Anton Malyshev Apr 10 '18 at 11:45
  • @AntonMalyshev Trailing return types are better-looking in many other circumstances and essential in others, so some of us prefer to use them in all cases, even for poor old `main()`. – underscore_d Jul 08 '20 at 16:56
-1

Compile time type match/mismatch can be controlled by std::enable_if. Implicit type conversion can be disabled by explicit keyword. All of copy and move constructors can be explicitly deleted to avoid copy and default constructor can explicitly marked as default. Edit: early answer takes into account first part of the question that "the user should only be able to pass in or get out the 'exposed' type" so all conversions must be explicit, however to complete the answer you can define a trivially copyable class and use that inside your proxy class May be the thing you intended:

#include  <cstdint>
#include <type_traits>
struct copyable{
    std::uint32_t number = 0x0;
};
class SomeProxyThing {

public:
    explicit operator  std::uint16_t()  const 
    {
        return storage.number & 0x0000FFFF;
    }
template <typename T, typename std::enable_if<std::is_same<T, std::uint16_t>::value, int>::type=0>
    SomeProxyThing& operator=(T value)
    {
        storage.number &= 0xFFFF0000;
        storage.number |= value;
        return *this;
    }

    SomeProxyThing()=default;
    SomeProxyThing(const SomeProxyThing&)=delete;
    SomeProxyThing(SomeProxyThing&&)=delete;
    SomeProxyThing& operator=(const SomeProxyThing& other) {
        this->storage.number = static_cast<std::uint16_t>(other.storage.number);
    }
    SomeProxyThing& operator=(SomeProxyThing&& other) {
        this->storage.number = static_cast<std::uint16_t>(other.storage.number);
    }

private:
    copyable storage;
};
int main()
{
    SomeProxyThing a, b;
    a = static_cast<std::uint16_t>(43);
    b = a; 
}
rahnema1
  • 15,264
  • 3
  • 15
  • 27
  • ...did you actually read my question before name-dropping a bunch of rudimentary concepts? Disabling conversion/assignment if types match is precisely the **opposite** of my problem: I'm specifically asking if there's a way to make it **preferred** over copy-assign. I know all about `delete` - as evidenced by how I tried it for copy-assign, where it's far more readable than `enable_if` - but doesn't solve the problem: it only prevents me accidentally using copy-assignment but does not favour any alternative. The success/failure cases in your code here are _exactly_ what I **don't** want to see – underscore_d Jul 12 '16 at 17:54
  • Thanks for trying. However, `Edit: early answer takes into account first part of the question that "the user should only be able to pass in or get out the 'exposed' type" so all conversions must be explicit` It does not follow. Again, ideally _all conversions would be **implicit**_. That's what I do and works fine in all cases except when LHS and RHS are related types. And `however to complete the answer you can define a trivially copyable class and use that inside your proxy class` What does this do? I still have a proxy class that isn't trivially copyable, except now it's harder to maintain! – underscore_d Jul 12 '16 at 20:32
  • @underscore_d also I changed the code. the storage object can now be trivially copied so in this way your suspection "that's not possible" may not be confirmed... – rahnema1 Jul 12 '16 at 20:47
  • Yes, the inner object is trivially copyable. But the proxy still isn't. So, now, I have the same result - a non-trivially-copyable proxy - but with the added baggage that it's been split into two layers. If you think through why I would want an object that is trivially copyable but that can expose values in a different format, you'd realise why the entire thing needs to be trivially copyable and why these two roles can't be split out. Suffice it to say that what I asked for can't be done. Which isn't ideal. But I'm afraid this answer paints a picture even further removed from my ideal. – underscore_d Jul 12 '16 at 20:51
  • no need that proxy be copyable. only storage would be copied. you mentioned that it is a oversimplified example. in your example stroage object of type uint16_t was trivially copyable but in this way more complex object can be trivially copied – rahnema1 Jul 12 '16 at 21:01
  • "no need that proxy be copyable" Sorry, whose question is this? Mine, so I'm the one who specifies the conditions, and one of those conditions is that the proxy must be trivially copyable. I'm running out of ways to say this – underscore_d Jul 12 '16 at 21:07
  • may some refinement to be applied to the question. so with this new condition your suspection my be confirmed :) – rahnema1 Jul 12 '16 at 21:19