Overloading the typecast operator for std::tuple

The preamble: Hey, let's say I have various representations of data that I want to convert seamlessly in-between in an any-to-any way. The representations are out of my control. My practical example is the object orientation in 3D: we have Quaternions, Euler angles, Angle-Axis, and Rotational matrices, everything in various classes from different libraries (which I have to use). For this, I built a proxy class which stores the value in one particular representation and can convert into it by overloaded constructors and out of it by overloaded typecast operators, just like this:

Eigen::Quaterniond eig_quaternion = AttitudeConvertor(roll, pitch, yaw);
tf2::Quaternion    tf2_quaternion = AttitudeConvertor(eig_quaternion);

The problem: So far, so good until I want to overload the typecast to std::tuple, which is handy, e.g., when returning the yaw, pitch and roll angles, which would look like this:

auto [roll2, pitch2, yaw2] = AttitudeConvertor(tf2_quaternion);

The class can be compiled, but the assignments to auto [a, b, c] and std::tie(a, b, c) do not work. A workaround can be made in the form of a dedicated function, which returns the tuple. Or by creating a custom class just for storing the three doubles. These work just fine, but it is not that seamless anymore.

I know that functions can not be overloaded by their return type. That is the reason why I created this proxy class. But is there any other way how the tuple could be returned? Even if it is for just a single variant of the tuple? Or should I approach this problem differently?

I prepared a minimum (non)working example in the theme of simpler number conversion:

#include <iostream>
#include <math.h>
#include <tuple>

using namespace std;

class NumberConvertor {

public:
  // | ---------------------- constructors ---------------------- |

  NumberConvertor(const int& in) {
    value_ = double(in);
  }

  NumberConvertor(const double& in) : value_(in){};

  // | ------------------- typecast operators ------------------- |

  operator int() const {
    return int(value_);
  }

  operator double() const {
    return value_;
  }

  // return the integer and the fractional part
  operator std::tuple<int, double>() const {

    int    int_part  = floor(value_);
    double frac_part = fmod(value_, int_part);
    return std::tuple(int_part, frac_part);
  }

  // | ------------------------ functions ----------------------- |

  // the workaround
  std::tuple<int, double> getIntFrac(void) const {

    int    int_part  = floor(value_);
    double frac_part = fmod(value_, int_part);
    return std::tuple(int_part, frac_part);
  }

private:
  double value_;  // the internally stored value in the 'universal representation'
};

int main(int argc, char** argv) {

  // this works just fine
  int    intval  = NumberConvertor(3.14);
  double fracval = NumberConvertor(intval);
  cout << "intval: " << intval << ", fracval: " << fracval << endl;

  // this does not compile
  // auto [int_part, frac_part] = NumberConvertor(3.14);

  // neither does this
  // int a;
  // double b;
  // std::tie(a, b) = NumberConvertor(3.14);

  // the workaround
  auto [int_part2, frac_part2] = NumberConvertor(3.14).getIntFrac();
  cout << "decimal and fractional parts: " << int_part2 << ", " << frac_part2 << endl;

  std::tie(int_part2, frac_part2) = NumberConvertor(1.618).getIntFrac();
  cout << "decimal and fractional parts: " << int_part2 << ", " << frac_part2 << endl;

  return 0;
};

Makefile:

main: main.cpp
    g++ -std=c++17 main.cpp -o main

all: main

Expected output:

intval: 3, fracval: 3
decimal and fractional parts: 3, 0.14
decimal and fractional parts: 1, 0.618

Upvotes: 3

Views: 208

Answers (3)

Thanks, everybody, for the great amount of help! Namely, to idclev-463035818 for his answer, which led me to complete a working solution. Thanks also to Jody Hagins, whose answer leads to an even more elegant solution. However, both solutions work only for the auto [a, b] = assignment. Luckily, here I found a way how to make the std::tie(a, b) = work:

  operator tuple<int&, double&>() {

    temp_int_    = floor(value_);
    temp_double_ = fmod(value_, temp_int_);

    return tuple<int&, double&>{temp_int_, temp_double_};
  }

The operator is defined as the tuple of references. The return statement looks slightly different than what I previously tried. But most importantly, the values are only correctly passed if the variables temp_int_ and temp_double_ are members of the class. I don't know how that works, but glad I am that it does.

Here is my version of the current minimum working example:

#include <iostream>
#include <math.h>
#include <tuple>

using namespace std;

class NumberConvertor {

public:
  // | ---------------------- constructors ---------------------- |

  NumberConvertor(const int& in) {
    value_ = double(in);
  }

  NumberConvertor(const double& in) : value_(in){};

  // | ------------------- typecast operators ------------------- |

  operator int() const {
    return int(value_);
  }

  operator double() const {
    return value_;
  }

  operator tuple<int&, double&>() {

    temp_int_    = floor(value_);
    temp_double_ = fmod(value_, temp_int_);

    return tuple<int&, double&>{temp_int_, temp_double_};
  }

  template <std::size_t I>
  constexpr auto get() {

    static_assert(I <= 1);

    if constexpr (I == 0) {
      return static_cast<int>(floor(value_));
    } else if constexpr (I == 1) {
      return static_cast<double>(fmod(value_, floor(value_)));
    }
  }

private:
  int    temp_int_;     // this is here for tieing the returned tuple
  double temp_double_;  // this is here for tieing the returned tuple
  double value_;        // the internally stored value in the 'universal representation'
};

template <>
struct std::tuple_size<NumberConvertor>
{ static constexpr int value = 2; };

template <>
struct std::tuple_element<0, NumberConvertor>
{ using type = int; };

template <>
struct std::tuple_element<1, NumberConvertor>
{ using type = double; };

int main(int argc, char** argv) {

  // this works just fine
  int    intval  = NumberConvertor(3.14);
  double fracval = NumberConvertor(intval);
  cout << "intval: " << intval << ", fracval: " << fracval << endl;

  auto [int_part, frac_part] = NumberConvertor(3.14);
  cout << "decimal and fractional parts: " << int_part << ", " << frac_part << endl;

  std::tie(int_part, frac_part) = NumberConvertor(3.14);
  cout << "decimal and fractional parts: " << int_part << ", " << frac_part << endl;

  return 0;
};

Upvotes: 0

Jody Hagins
Jody Hagins

Reputation: 28409

If I could, I'd just add this as a comment to the answer from idclev.

According to Nico's advice in http://www.cppstd17.com, the member function version for providing get access should be avoided.

Furthermore, there is no need to create extra std::tuple instantiations when not necessary.

You need three things to make a class act like a tuple: tuple_size, tuple_element, and get.

template <>
struct std::tuple_size<NumberConvertor>
{
    static constexpr int value = 2;
};
template <>
struct std::tuple_element<0, NumberConvertor>
{
    using type = int;
};
template <>
struct std::tuple_element<1, NumberConvertor>
{
    using type = double;
};

template <std::size_t I>
constexpr auto
get(NumberConvertor const &x)
{
    static_assert(I <= 1);
    if constexpr (I == 0) {
        return static_cast<int>(x);
    } else if constexpr (I == 1) {
        return static_cast<double>(x);
    }
}

Note that this gives read-only access, but that seems to be just what is desired in this case.

Upvotes: 2

463035818_is_not_an_ai
463035818_is_not_an_ai

Reputation: 122595

Jarod42s hint to binding_a_tuple-like_type made me come up with the following.

I basically make your NumberConvertor act like a tuple.

using as_tuple_type = std::tuple<int,double>;

For convenience an alias template can be used:

template <size_t i>
using nth_type = typename std::tuple_element_t<i,as_tuple_type>;

Using that, we can provide a get method:

struct NumberConvertor {
  NumberConvertor(const int& in) : value_(in) {}
  NumberConvertor(const double& in) : value_(in) {};
  template <size_t i> nth_type<i> get();
private:
  double value_;
};

template <> nth_type<0> NumberConvertor::get<0>() { return value_;}
template <> nth_type<1> NumberConvertor::get<1>() { return value_;}

The specializations aren't really needed here, but I suppose for the real scenario this is not the case.

Finally we provide specializations for std::tuple_size and std::tuple_element:

template <> 
struct std::tuple_size<NumberConvertor> : std::tuple_size<as_tuple_type> 
{};
template <size_t i> 
struct std::tuple_element<i,NumberConvertor> : std::tuple_element<i,as_tuple_type> 
{};

Now this will work:

int main(int argc, char** argv) {
    auto [int_part, frac_part] = NumberConvertor(3.14);
    std::cout << int_part << " " << frac_part;
};

Complete example

Upvotes: 2

Related Questions