Reputation: 31
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
Reputation: 31
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
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
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;
};
Upvotes: 2