3

I want to write an aggregate template struct with custom assignment operator, like this:

template <typename T>
struct Foo {
    Foo() = default;
    Foo(const Foo&) = default;

    Foo& operator=(const Foo& f) { ... }

    ...
};

Now, if T is a const-qualified type I want to have:

Foo& operator=(const Foo& f) = delete;

The only way I can think of is to specialize Foo struct:

template<T> struct Foo<const T> {
   Foo& operator=(const Foo& f) = delete;
   ... a lot of code ...
}

But to specialize this struct I must copy-paste all the remaining code (aggregate means no inheritance - at least before C++17 and no possibility to move common code to base class).

Is there any better way to do that?

max66
  • 65,235
  • 10
  • 71
  • 111
Igor
  • 1,307
  • 1
  • 10
  • 19
  • 2
    What exactly is your assignment operator going to do? And why? –  Aug 06 '18 at 19:40
  • 1
    Possible duplicate of [Contitional enable an alternative assignment operator](https://stackoverflow.com/questions/9889101/contitional-enable-an-alternative-assignment-operator) – Alan Birtles Aug 06 '18 at 19:44
  • 1
    If `Foo` holds a `T` member then it would automatically lose the copy constructor if `T` is `const`. – NathanOliver Aug 06 '18 at 19:47
  • @NeilButterworth it will modify some global state. For the purpose of this example, let's say it wil just log some information. – Igor Aug 06 '18 at 19:48
  • @NathanOliver That would be true, if I wouldn't define my own operator= which executes some extra code in comparison to compiler-generated one. – Igor Aug 06 '18 at 19:50
  • I'm not sure - why would you want to prevent at all? If there is a member of type T (now const), assignment will fail to compile anyway; if there isn't, then what's the matter of a copy? – Aconcagua Aug 06 '18 at 20:07
  • @AlanBirtles If I would try to adapt solution from this other question I would have to declare 2 versions of operator= using std::enable_if (one custom and one deleted). But C++ standard says that operator= should not be template, and compiler would generate it's own version. In this other question there was one non-template assignment operator, so there was no problem. – Igor Aug 06 '18 at 20:19

2 Answers2

7

I propose a sort of self inheritance: the const specialization that inherit from the generic version

template <typename T>
struct Foo
 {
   Foo() = default;
   Foo(Foo const &) = default;

   Foo& operator= (Foo const & f) { return *this; }
 };

template <typename T>
struct Foo<T const> : public Foo<T>
 {
   Foo& operator= (Foo const & f) = delete;
 };

This way your const specialization inherit all from the generic version, so there is no need of copy and past all the common code, except the operator=() that is deleted.

The following is a full example

template <typename T>
struct Foo
 {
   Foo() = default;
   Foo(Foo const &) = default;

   Foo& operator= (Foo const & f) { return *this; }
 };

template <typename T>
struct Foo<T const> : public Foo<T>
 {
   Foo& operator=(Foo const & f) = delete;
 };

int main () 
 {
   Foo<int>        fi;
   Foo<int const>  fic;

   fi  = fi;   // compile
   // fic = fic; // compilation error
 }
max66
  • 65,235
  • 10
  • 71
  • 111
  • That's really nice, I like this solution! The only problem is that Foo is no longer an aggregate (before C++17). But I guess there's no way around that. – Igor Aug 06 '18 at 20:25
  • @Igor - you're right: unfortunately use inheritance; but I think it's difficult to avoid it. An alternative idea: what if `operator=()` is ever enabled but call a function that is enabled only if `T` isn't const? – max66 Aug 06 '18 at 21:21
  • Yes, that would work, but this wouldn't play well with type traits, for example std::is_copy_assignable>::value would be true. – Igor Aug 07 '18 at 13:43
2

I think you can fully hide the assignment with CRTP. Structurally, it is similar to the self-inheritance technique, but it uses static polymorphism to implement the assignment operator in the base. The const specialized version is deleted, so attempts to invoke the assignment operator will fail.

template <typename D>
struct FooCRTP {
    D & derived () { return *static_cast<D *>(this); }
    D & operator = (const D &rhs) { return derived() = rhs; }
};

template <typename T> struct Foo : FooCRTP<Foo<T>> {};

template <typename T>
struct Foo<const T> : FooCRTP<Foo<const T>>
{
    Foo & operator = (const Foo &) = delete;
};

One advantage of the CRTP version over the self-inheritance technique is that in the CRTP solution, the base class assignment is implemented using the derived's assignment. However, in the self-inheritance technique, the base class assignment is its own implementation, so it may be invoked unexpectedly. For example:

Foo<int> f_int;
Foo<const int> f_cint;

f_cint.Foo<int>::operator=(f_int);

The above code will fail to compile with CRTP, but the compiler will not complain using the self-inheritance technique.

jxh
  • 69,070
  • 8
  • 110
  • 193
  • Could you expand on how `f_cint.Foo::operator=(f_int);` is legal / under which circumstances it's legal? To me it looks as if we are trying to assign a base class object (`f_int`) to an object of the derived type (`f_cint`), but this obviously works, so I'm probably not just following what's happening. Thanks! – dfrib Aug 06 '18 at 21:22
  • @dfri: I pointing out a weakness in max66's solution. It is my assumption the OP does not want `Foo` to have any assignment operator defined. In the CRTP solution presented, that direct invocation will not work. – jxh Aug 06 '18 at 21:23
  • Yes I followed that (and upvoted this answer, good use of CRTP!), but I'm specifically wondering if you could expand what's actually happening when making use of that weakness? I have never seen that syntax used before (explicitly calling `operator+` but for another specialization than the target of the assignment). I should probably make clear (in case that was missed in my first comment) that I am not questioning this, I just don't understand it, and would be curious and grateful if you could expand/explain the mechanisms in use for that assignment. – dfrib Aug 06 '18 at 21:27
  • Are we actually assigning a `Foo` to a `Foo`, or is a `Foo` converted to a `Foo` and thereafter assigned to a `Foo` (circumventing that `Foo::operator=` has been deleted)? Or some other way around? – dfrib Aug 06 '18 at 21:29
  • What I am trying to say is that the CRTP solution is using static polymorphism. So, if the derived class deletes the assignment operator, attempts to use the parent's implementation of the assignment operator will fail. In the self-inheritance solution, the parent's implementation is always available, even if the child tries to delete its own. – jxh Aug 06 '18 at 21:34
  • Ah, I was probably a bit unclear in my question, sorry for that, but _"attempts to use the parent's implementation"_ answered it for me: so the `f_cint.Foo::operator=(f_int);` is simply accessing the base assignment operator from the derived type, in so possibly doing a "partial assignment" of `f_int` onto `f_cint`? I think `` vs `` confused me, when [I should've just focused on `Base` vs `Derived`](https://gist.github.com/dfrib/ef3069dc1d41d40a2e2440ab717a3f7d). Thanks! – dfrib Aug 06 '18 at 21:47
  • Well, yes, for the self-inheritance solution, the base class's implementation is the "self" (`Foo`) without the `const` in the template parameter. For the CRTP solution, the base class is different (`FooCRTP`), but even if you addressed the base class by the right name, it would still fail to compile since the parent class is just trying to invoke the child's implementation. – jxh Aug 06 '18 at 22:53