28

Consider a simple int Wrapper class with overloaded multiplication operator*= and operator*. For "old-style" operator-overloading, one can define operator* in terms of operator*=, and there are even libraries like Boost.Operators and its modern incarnation df.operators by @DanielFrey that reduce the boilerplate for you.

However, for compile-time computations using the new C++11 constexpr, this convenience disappears. A constexpr operator* cannot call operator*= because the latter modifies its (implicit) left argument. Furthermore, there is no overloading on constexpr, so adding an extra constexpr operator* to the existing operator* results in an overload resolution ambiguity.

My current approach is:

#include <iostream>

struct Wrap
{
    int value;    

    Wrap& operator*=(Wrap const& rhs) 
    { value *= rhs.value; return *this; }

    // need to comment this function because of overloading ambiguity with the constexpr version
    // friend Wrap operator*(Wrap const& lhs, Wrap const& rhs)
    // { return Wrap { lhs } *= rhs; }    

    friend constexpr Wrap operator*(Wrap const& lhs, Wrap const& rhs)
    { return { lhs.value * rhs.value }; }
};

constexpr Wrap factorial(int n)
{
    return n? factorial(n - 1) * Wrap { n } : Wrap { 1 };    
}

// want to be able to statically initialize these arrays
struct Hold
{
    static constexpr Wrap Int[] = { factorial(0), factorial(1), factorial(2), factorial(3) };
};

int main() 
{
    std::cout << Hold::Int[3].value << "\n"; // 6
    auto w = Wrap { 2 };
    w *= Wrap { 3 };
    std::cout << w.value << "\n"; // 6
}

Live output here. My problems with this are:

  • duplication of the multiplication logic in both operator*= and operator*, instead of operator* being expressed in terms of operator*=
  • hence, Boost.Operators no longer works to reduce the boilerplate for writing many other arithmetic operators

Question: is this the recommended C++11 way of having both a run-time operator*= and mixed run-time/compile-time constexpr operator*? Does C++14 change anything here to e.g. reduce the logic duplication?

UPDATE: The answer by @AndyProwl is accepted as idiomatic but as per suggestion of @DyP, in C++11 one could reduce the logic duplication at the expense of an extra assignment and counter-intuitive style

    // define operator*= in terms of operator*
    Wrap& operator*=(Wrap const& rhs) 
    { *this = *this * rhs; return *this; }
Community
  • 1
  • 1
TemplateRex
  • 69,038
  • 19
  • 164
  • 304
  • What's your use for a 'regular' overload if it can be `constexpr`? IIRC `constexpr` will gracefully degrade to _runtime execution_ in non-`constexpr` context. – sehe Jul 19 '13 at 11:41
  • 1
    @sehe you cannot have a `constexpr operator*=`, and so `constexpr operator*` cannot call this, and instead needs to duplicate the logic of extracting fields etc. – TemplateRex Jul 19 '13 at 11:45
  • 1
    Ah, I'm beginning to see your real question. It's ***not*** about having non-`constexpr` overloads (you don't need them!) but rather about not being able to share code because `*=` can't be `constexpr`. Good thing I already +1ed on good faith :) – sehe Jul 19 '13 at 11:46
  • Is my interpretation correct that you could define `operator *=` in terms of `operator *` and assignment, but this is bad because of possible overhead / performance (not here, but in the general case)? – dyp Jul 19 '13 at 12:16
  • 1
    @DyP you mean `*this = *this * rhs; return *this;` as body for `operator*`? I think that would be ineficient, but maybe the compiler will optimize and generate the original code? – TemplateRex Jul 19 '13 at 12:24
  • 2
    (As body for `operator *=`, I guess that's a typo?) The types you wrap need a `constexpr` copy-ctor anyway, so this assignment shouldn't be complicated (no memory allocation etc). Which means that also `move` won't probably have many advantages over `copy`, but it shouldn't be hard for the compiler to optimize this assignment, given that it shouldn't have side effects. – dyp Jul 19 '13 at 12:37

1 Answers1

23

I could not find an idiomatic solution for C++11 (although as a workaround, DyP's suggestion seems acceptable to me).

In C++14 however, where constexpr does not imply const (see Annex C.3.1 of the C++14 Standard Draft n3690), you could simply define both operator *= and operator * as constexpr, and define the latter in terms of the former, as usual:

struct Wrap
{
    int value;    

    constexpr Wrap& operator *= (Wrap const& rhs) 
    { value *= rhs.value; return *this; }

    friend constexpr Wrap operator * (Wrap const& lhs, Wrap const& rhs)
    { return Wrap(lhs) *= rhs; }    
};

Here is a live example, where the above program is being compiled with -std=c++1y on Clang - unfortunately, GCC does not seem to implement this rule yet.

Community
  • 1
  • 1
Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • what do you think of DyP's suggestion of reversing the dependency at the cost of an assignment? – TemplateRex Jul 19 '13 at 12:32
  • 5
    @TemplateRex: Yes, I thought about that myself, but I did not post it as a solution because it would go against the standard practice. Reverting a guideline when an enabling change is ready for C++14 would be inappropriate IMO. However, as a C++11 workaround, it should be acceptable. I honestly do not think performance would be an issue here - I am no compiler expert, but I would expect the optimizer to perform heavy inlining for such simple functions. Also, I try to stay clear of premature optimization and avoid rejecting a design just because of assumptions on performance. – Andy Prowl Jul 19 '13 at 12:38
  • The `constexpr` no longer implies `const` aside, I'm puzzled **why** it works: `operator*=` is both multilined and mutates `this`, yet it is being called during compile-time. Are both those restrictions *also* removed in C++14? Do you have a working paper quote? – TemplateRex Jul 20 '13 at 13:28
  • 2
    @TemplateRex: Yes, some `constexpr` restrictions have been lifted, including the fact that the function has to be a single `return` statement. Not sure about the working paper, but if you compare paragraph 7.1.5/3 of the C++11 and C++14 Standard Draft n3690 you can see what the differences are – Andy Prowl Jul 20 '13 at 13:48
  • 1
    You can even write `factorial` non-recursively in C++1y -- [live example](http://coliru.stacked-crooked.com/a/0cc9351e2e0dd0bc). – Richard Smith Nov 04 '13 at 03:49
  • Ah, this "C++14 and later not having `constexpr` imply `const`" must be why some older libraries (such as for example lmath's `half` datatype) don't have `constexpr` for `operator+=()` and others... – saxbophone May 21 '23 at 20:05