为 std::tuple 重载类型转换运算符

3

前言: 嘿,假设我有各种数据的表示方式,并且希望以任意方式轻松地在它们之间进行转换。这些表示是我无法控制的。我的一个实际例子是 3D 中的对象定向:我们有四元数、欧拉角、角轴和旋转矩阵,都来自不同库的各种类(我必须使用)。为此,我建立了一个代理类,它将值存储在一种特定的表示中,并可以通过重载的构造函数将其转换到该表示形式并通过重载的类型转换运算符将其转出,就像这样:

Eigen::Quaterniond eig_quaternion = AttitudeConvertor(roll, pitch, yaw);
tf2::Quaternion    tf2_quaternion = AttitudeConvertor(eig_quaternion);
问题:到目前为止,一切都很好,直到我想要重载std::tuple的类型转换函数,这在返回yawpitchroll角度时非常方便,代码如下:
auto [roll2, pitch2, yaw2] = AttitudeConvertor(tf2_quaternion);

该类可以编译,但对于`auto [a, b, c]`和`std::tie(a,b,c)`的赋值不能正常工作。解决方法可以通过创建一个专用函数来返回元组或者创建一个自定义类来存储三个double。这些方法都可以正常工作,但不够流畅。我知道函数不能以其返回类型进行重载,这就是我创建代理类的原因。但是,是否有其他方法可以返回元组呢?即使只是针对元组的一个变体?还是我应该用不同的方式来解决这个问题?
我准备了一个最小(非)工作示例,主题为更简单的数字转换:
#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

预期输出:

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

1
binding_a_tuple-like_type 可能会有所帮助。 - Jarod42
3个回答

2
Jarod42提到类似元组的类型绑定后,我想出了以下内容。
基本上,我让你的NumberConvertor像元组一样工作。
using as_tuple_type = std::tuple<int,double>;

为了方便起见,可以使用别名模板:

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

通过这样,我们可以提供一个get方法:

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_;}

这里并不真正需要专业知识,但我假设在实际场景下是必要的。

最后,我们为std::tuple_sizestd::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> 
{};

现在这将会起作用:

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

完整示例


1
既然已经有了my_return_trait,我想我们可以直接这样做:template<std::size_t I> struct std::tuple_element<I, NumberConverter> { using type = my_return_type<I>::type; }; - L. F.
@L.F. 当你写下某些内容并不确定时,然后有人立即指出了那个细节,这种感觉真是...哈哈。我想是的,让我试试... - 463035818_is_not_a_number
@L.F. 嗯,感谢您的评论,让我意识到原始代码过于冗长和重复。 - 463035818_is_not_a_number
注意:std::tie(int_part, frac_part) = NumberConvertor(1.618); 仍然不能正常工作,它需要转换为 std::tuple<int&, double&>(可能无法提供)。 - Jarod42
@Jarod42 是的,看起来OP找到了一种方法,但它需要将值作为成员返回(否则你怎么能得到引用)。据我所知,一旦你将它们作为成员拥有,就有更简单的方法启用结构化绑定,需要进一步探索一下... - 463035818_is_not_a_number

2
如果可以的话,我会将这个作为评论添加到idclev的答案中。
根据Nico在http://www.cppstd17.com中的建议,应该避免使用提供get访问的成员函数版本。
此外,在不必要时不需要创建额外的std::tuple实例。
您需要三个东西来使一个类像一个元组:tuple_size、tuple_element和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);
    }
}

请注意,这只提供了只读访问权限,但在这种情况下似乎正是所需的。

1
为什么应该避免使用成员函数版本? - Barry
除了一般建议更喜欢使用自由函数而不是成员函数之外,我并没有看到成员函数 get 存在问题。 - 463035818_is_not_a_number
撇开我的个人观点不谈,这是他书中的引用:“C++17标准还允许我们将这些get<>()函数定义为成员函数,但这可能是一个疏忽,不应该使用。” - Jody Hagins
@JodyHagins 感谢您的报价。我有点困惑为什么称其为“疏忽”,因为据我所知,它不是自动出现并需要明确禁用的东西,无论如何,我想这只是通常的“优先使用非成员函数”。 - 463035818_is_not_a_number
不幸的是,这就是他对此的全部说法。我的个人观点是,除了更喜欢自由函数之外,使用自由函数替代方案可以使元组类似的结构更好地发挥作用。例如,std::tuple没有get成员函数。通过为所有想要具有元组类似接口的类使用自由函数,可以使一切变得更加容易。然后,你只需要使用using std::get(直到c++20),然后你就可以在任何类似于元组的东西上调用get<N>(obj)。成员函数版本只能与结构化绑定很好地配合使用。 - Jody Hagins
哦,非常感谢。这当然是反对编写自己的成员get的有力论据。我甚至没有意识到std::tuple没有它作为成员。我在生产代码中被困在c++11中。只有在这样有趣的练习中,我才接触到更现代的东西。 - 463035818_is_not_a_number

0

感谢大家的帮助!特别是感谢idclev-463035818提供的答案,让我完成了一个可行的解决方案。还要感谢Jody Hagins,他的答案提供了更加优雅的解决方案。然而,这两种方法只适用于auto [a, b] =赋值。幸运的是,在这里我找到了一种方法,可以让std::tie(a, b) =起作用:

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

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

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

运算符被定义为引用的元组。返回语句看起来跟我之前尝试的有点不同。但最重要的是,只有当变量temp_int_temp_double_是类的成员时,值才会正确传递。我不知道这是如何工作的,但很高兴它确实可以。

这是当前最小工作示例的版本:

#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;
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接