元组

Python 的元组与列表类似,区别在于其元素不可修改。元组的最大特点是可存放任意类型,所以我们可使用其将不相关的变量绑定为一个整体。C++ 作为编译型语言,往往通过模板(编译期),或通过类似于函数闭包的方法(运行期,如std::any),实现不定类型元组。C++ 标准库中的元组类型,是通过模板多态实现的。

在 python 中,元组创建很简单,只需要在括号中添加元素,并使用逗号隔开即可。
就像这样:

t1 = ('Hello', 111)
t2 = "How", "are", "you"

而 C++ 也有现成的元组类型std::tuple,在<tuple>中定义。

auto t1 = std::make_tuple ("My", 22);
auto t2 = std::make_tuple ("Name", "is");

元组与数组类似,下标索引从 0 开始。

访问元组

我们知道,Python 中元组可以使用下标索引来访问元组中的值。

t3 = ('Yukino', 'Amamiya')
print ('First Name', t3[-1])
print ('Last Name', t3[0])

在编译期,C++ 也可通过 std::get<N>()来获取指定位置的变量。

auto t3 = std::make_tuple ('You', 'can', 'call me');
std::cout << std::get<0>(t3) << std::endl;
std::cout << std::get<1>(t3) << std::endl;

而在运行时期,C++ 需要通过递归的方式来逐一比较编译器和运行期常量的值,从而获取指定下标的元素。而递归会导致找到时所处位置在函数调用栈的深处,很难返回,故在这里我通过赋值的方式返回。
注意代码中赋值时使用了模板类型萃取条件,从而避免元组中有不是想要的下标元素,类型不匹配导致编译错误。
就像这样:

/* Runtime subscript access tuples */
constexpr int /* Recursive termination condition */
mcl_tuple_visitor (size_t, std::tuple<> const&, void*) { return 0; }
template <typename lhs_t, typename rhs_t>
constexpr typename std::enable_if<!std::is_convertible<rhs_t, lhs_t>::value, void>::type
mcl_copy (lhs_t* , rhs_t* ) /* The specified subscript element type cannot be */
{ throw std::bad_cast (); } /* implicitly converted to the target type */
template <typename lhs_t, typename rhs_t>
constexpr typename std::enable_if<std::is_convertible<rhs_t, lhs_t>::value, void>::type
mcl_copy (lhs_t* lhs, rhs_t* rhs) noexcept
{ const_cast<typename std::remove_const<decltype(*lhs)>::type>(*lhs) = *rhs; }
template <typename visit_t, typename Tv, typename... Ts> void
mcl_tuple_visitor (size_t index, std::tuple<Tv, Ts...> const& t, visit_t* ptr) {
if (index >= (1 + sizeof...(Ts)))
throw std::invalid_argument ("Bad Index");
else if (index > 0)
mcl_tuple_visitor (index - 1, reinterpret_cast<std::tuple<Ts...> const&>(t), ptr);
else mcl_copy (ptr, &std::get<0>(t));
}

使用operator<<输出时,由于模板不指定目标类型,故需要进行特殊化处理。

/* Outputs specified tuple elements at run time */
struct mcl_do_is_printable {
template<typename lhs_t, typename rhs_t,
typename = decltype(*static_cast<lhs_t*>(0) << *static_cast<rhs_t*>(0))>
static std::integral_constant<bool, true> test(int); /* only declaration */
template<typename, typename>
static std::integral_constant<bool, false> test(...);
};
template<typename lhs_t, typename rhs_t>
struct mcl_is_printable
: public mcl_do_is_printable {
typedef decltype(test<lhs_t, rhs_t>(0)) type;
}; /* Judge whether the target type can be output */
template <typename lhs_t, typename rhs_t>
constexpr typename std::enable_if<mcl_is_printable<lhs_t, rhs_t>::type::value, void>::type
mcl_oss_print (lhs_t* lhs, rhs_t* rhs) noexcept
{ *lhs << *rhs; }
template <typename lhs_t, typename rhs_t>
constexpr typename std::enable_if<!mcl_is_printable<lhs_t, rhs_t>::type::value, void>::type
mcl_oss_print (lhs_t* , rhs_t* )
{ throw std::ios_base::failure ("The specified type does not overload operator<< . "
"(with type = " + std::string (typeid(rhs_t).name()) + ")"); }
constexpr int
mcl_tuple_printer (size_t, std::tuple<> const&, void*) noexcept{ return 0; }
template <typename os_t, typename Tv, typename... Ts> void
mcl_tuple_printer (size_t index, std::tuple<Tv, Ts...> const& t, os_t* oss) {
if (index >= (1 + sizeof...(Ts)))
throw std::invalid_argument ("Bad index");
else if (index > 0)
mcl_tuple_printer (index - 1, reinterpret_cast<std::tuple<Ts...> const&>(t), oss);
else mcl_oss_print (oss, &std::get<0>(t));
}
/* Use the characteristics of compile time extension to obtain the elements with
specified subscripts through recursion at run time */

元组大小比较

Python 中,我们可以直接比较两个元组的大小,其规则是:

如果比较的元素是同类型的,则比较其值,返回结果。
如果两个元素不是同一种类型,则检查它们是否是数字。

  • 如果是数字,执行必要的数字强制类型转换,然后比较。
  • 如果有一方的元素是数字,则另一方的元素”大”(数字是”最小的”)
  • 否则,通过类型名字的字母顺序进行比较。

如果有一个列表首先到达末尾,则另一个长一点的列表”大”。
如果我们用尽了两个列表的元素而且所 有元素都是相等的,那么结果就是个平局,就是说返回一个 0。

而对于 C++ 中的 std::tuple,仅支持类型完全相同的两个元组之间的比较。为实现 python 中元组的比较效果,我们可以通过如下方式实现:

/* Compare between tuples of defferent types */
template <typename... lhs_t, typename... rhs_t>
constexpr typename std::enable_if<!sizeof...(lhs_t) && sizeof...(rhs_t), int>::type
cmp (std::tuple<lhs_t...> const& , std::tuple<rhs_t...> const& ) noexcept
{ return -1; } /* lhs reaches the end first, then rhs is larger */
template <typename... lhs_t, typename... rhs_t>
constexpr typename std::enable_if<sizeof...(lhs_t) && !sizeof...(rhs_t), int>::type
cmp (std::tuple<lhs_t...> const& , std::tuple<rhs_t...> const& ) noexcept
{ return 1; } /* rhs reaches the end first, then lhs is larger */
template <typename... lhs_t, typename... rhs_t>
constexpr typename std::enable_if<!sizeof...(lhs_t) && !sizeof...(rhs_t), int>::type
cmp (std::tuple<lhs_t...> const& , std::tuple<rhs_t...> const& ) noexcept
{ return 0; } /* lhs and rhs reaches the end at the same time, this means two tuples
are exactly equal. */
template <typename lhsf, typename rhsf, typename... lhs_t, typename... rhs_t>
constexpr typename std::enable_if<
!(std::is_same<lhsf, rhsf>::value
|| (std::is_arithmetic<lhsf>::value && std::is_arithmetic<rhsf>::value)
), int>::type
cmp (std::tuple<lhsf, lhs_t...> const&, std::tuple<rhsf, rhs_t...> const&) noexcept {
return std::is_arithmetic<lhsf>::value ? -1
: ( std::is_arithmetic<rhsf>::value ? 1 : (sizeof (lhsf) > sizeof (rhsf)) );
} /* The end is not reached and the first element type is different. Compare the type,
and the number type is smaller. Otherwise, press sizeof for comparison. */
template <typename lhsf, typename rhsf, typename... lhs_t, typename... rhs_t>
constexpr typename std::enable_if<
(std::is_same<lhsf, rhsf>::value
|| (std::is_arithmetic<lhsf>::value && std::is_arithmetic<rhsf>::value)
), int>::type
cmp (std::tuple<lhsf, lhs_t...> const& lhs, std::tuple<rhsf, rhs_t...> const& rhs) noexcept {
return std::get<0>(lhs) == std::get<0>(rhs) ?
cmp (reinterpret_cast<std::tuple<lhs_t...> const&>(lhs),
reinterpret_cast<std::tuple<rhs_t...> const&>(rhs))
: (std::get<0>(lhs) > std::get<0>(rhs)) - (std::get<0>(lhs) < std::get<0>(rhs));
} /* If the end is not reached and the first element type is the same, compare whether the
first element of two tuples is equal. If it is not equal, the comparison result is
returned, and if it is equal, the next element is compared recursively. */

元组长度

Python 中,我们可以像这样获取元组长度,或访问元组中的指定位置的元素,如下所示:
元组:

    N = ('Yukinochan', '雨雪酱', 'みぞれちゃん')

Python 表达式 结果 描述
len(N) 3 获取元组长度
N[2] ‘みぞれちゃん’ 读取第三个元素
N[-2] ‘雨雪酱’ 反向读取,读取倒数第二个元素

对于 C++,获取元组长度可使用std::tuple_size

auto N = std::make_tuple ('雨宮雪乃', '雨雪ちゃん');
std::cout << "len = " << std::tuple_size<decltype(N)>::value;

简易对其进行一个封装:

template <typename... T>
constexpr size_t len (std::tuple<T...> const&) noexcept
{ return sizeof... (T); }

元组索引

而运行时期按下标进行访问,我们上面已经实现了核心代码,现在只需要创建一个继承自std::tuple的类,姑且取名叫做pytuple

template <typename... T>
class pytuple
: public std::tuple<T...> {
public:
constexpr pytuple () = default;
constexpr pytuple (const pytuple<T...>&) = default;
constexpr pytuple (pytuple&&) = default;
pytuple<T...>& operator= (pytuple<T...> const&) = default;
pytuple<T...>& operator= (pytuple<T...>&&) = default;
constexpr pytuple (std::tuple<T...> const& tp)
: std::tuple<T...> (tp) { }
constexpr pytuple (T&&... parms)
: std::tuple<T...> (std::make_tuple (std::forward<T>(parms)...)) { }
};

定义函数maktuple,以便于创建元组对象。

template <typename... Ts>
constexpr pytuple<typename std::decay<typename std::remove_reference<Ts>::type>::type...>
maktuple (Ts&&... agv) noexcept {
return std::make_tuple (std::forward<Ts>(agv)...);
}

并重载pytuple::operator[],使其接受下标访问即可。

constexpr proxy_t<T...> operator[] (long long index) const noexcept
{ return proxy_t <T...>(*this, index >= 0 ? index : sizeof...(T) + index); }

这里返回一个pytuple::proxy_t,用于接收需要转换的类型为参数。

template <typename... pt>
class proxy_t {
friend pytuple;
friend class pytuple::iterator;
std::tuple<pt...> const& m_ptr;
size_t index;
constexpr proxy_t (std::tuple<pt...> const& m, size_t i) noexcept
: m_ptr (m), index (i) { }
public:
template<typename cv>
inline operator cv const () const{
cv v; /* ignore Int-unini, may not have default constructor */
mcl_tuple_visitor<cv> (index, m_ptr, &v);
return v;
}
friend inline std::ostream&
operator<< (std::ostream& os, proxy_t<pt...> const& rhs) {
mcl_tuple_printer<std::ostream> (rhs.index, rhs.m_ptr, &os);
return os;
}
friend inline std::wostream&
operator<< (std::wostream& os, proxy_t<pt...> const& rhs) {
mcl_tuple_printer<std::wostream> (rhs.index, rhs.m_ptr, &os);
return os;
}
};

元组遍历

在 Python 中,我们可以像这样对元组进行遍历输出:

tu = ('Thank', 'you', 'for', 'making', 'use', 'of')
for i in tu:
print (i)

在 C++ 中也有基于范围的循环。我们只需要提供beginend方法即可利用这一语法糖。

constexpr iterator begin () const noexcept{ return iterator {*this, 0}; }
constexpr iterator end () const noexcept{ return iterator {*this, sizeof...(T)}; }

这里返回一个pytuple::iterator用于进行索引。

class iterator {
std::tuple<T...> const& m_ptr;
size_t index;
constexpr iterator (std::tuple<T...> const& m, size_t i) noexcept
: m_ptr (m), index (i) { }
friend pytuple;
public:
constexpr pytuple::proxy_t<T...> operator* () noexcept
{ return pytuple::proxy_t<T...>(m_ptr, index); }
constexpr bool operator!= (iterator const& rhs) const noexcept
{ return this->index != rhs.index; }
inline iterator& operator++ () noexcept{ ++ index; return *this; }
};

如此以来,我们就可以在 C++ 中这样写:

auto tp = mcl::maktuple ('this', 'graphics', 'library')
for (auto i: tp)
std::cout << i << ' ';

如果只是用于输出,可以利用递归来优化。

private:
template <typename char_type, size_t i = 1>
constexpr typename std::enable_if<
(i >= sizeof...(T)), std::basic_ostream<char_type>& >::type
str_ (std::basic_ostream<char_type>& ss) const noexcept
{ return ss << ")"; }
template <typename char_type, size_t i = 1>
constexpr typename std::enable_if<
(i < sizeof...(T)), std::basic_ostream<char_type>& >::type
str_ (std::basic_ostream<char_type>& ss) const noexcept
{ return ss << ", " << std::get<i>(*this), str_<char_type, i + 1>(ss); }
public:
friend constexpr std::ostream&
operator<< (std::ostream& os, pytuple<T...> const& tu) {
return sizeof...(T) ? (
os << '(' << std::get<0>(tu),
(sizeof...(T) == 1) ? (os << ",)") : (tu.str_ (os))
) : (os << "()");
}
friend constexpr std::wostream&
operator<< (std::wostream& os, pytuple<T...> const& tu) {
return sizeof...(T) ? (
os << '(' << std::get<0>(tu),
(sizeof...(T) == 1) ? (os << L",)") : (tu.str_ (os))
) : (os << L"()");
}

这样以来,输出一个元组,我们还可以写:

auto tp = mcl::maktuple ("GPL-3.O","©", "Yukino Amamiya");
std::cout << tp << std::endl;

总结

至此,我们就实现了一个简易的 Python 元组。它可以遍历、可以嵌套、可以按下标访问。当然,Python 元组还有很多其他特性,比如切片、复制等,由于这些功能只能通过运行时闭包实现,极大的降低了效率,而图形库 mclib 中元组仅是作为部分函数的返回值使用,用不到这么多功能,故并未支持。

这里列出支持的操作(注释为输出结果):

auto t1 = mcl::maktuple (1, 2, "Hello");
auto t2 = mcl::maktuple (0.2, 99, "World");
auto t3 = mcl::maktuple (t1);
std::cout << t1 << '\n'; /* (1, 2, Hello) */
std::cout << t3 << '\n'; /* ((1, 2, Hello),) */
std::cout << mcl::cmp(t2, t1) << '\n'; /* -1 */
std::cout << t1[-1] << '\n'; /* Hello */
std::cout << t2[2] << '\n'; /* World */
std::cout << mcl::len(t2) << '\n'; /* 3 */
for (auto i: t2)
std::cout << i << '\n'; /* 0.2\n99\nWorld */
float m = t2[-2]; /* m = 99.f */

关关难过关关过,前路漫漫亦灿灿。