Python 元组 在 C++ 中的简易实现
元组
Python 的元组与列表类似,区别在于其元素不可修改。元组的最大特点是可存放任意类型,所以我们可使用其将不相关的变量绑定为一个整体。C++ 作为编译型语言,往往通过模板(编译期),或通过类似于函数闭包的方法(运行期,如std::any
),实现不定类型元组。C++ 标准库中的元组类型,是通过模板多态实现的。
在 python 中,元组创建很简单,只需要在括号中添加元素,并使用逗号隔开即可。
就像这样:
t1 = ('Hello', 111)
t2 = "How", "are", "you"
而 C++ 也有现成的元组类型std::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
来获取指定位置的变量。
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++ 需要通过递归的方式来逐一比较编译器和运行期常量的值,从而获取指定下标的元素。而递归会导致找到时所处位置在函数调用栈的深处,很难返回,故在这里我通过赋值的方式返回。
注意代码中赋值时使用了模板类型萃取条件,从而避免元组中有不是想要的下标元素,类型不匹配导致编译错误。
就像这样:
#!/mclib/src/pyobj/tuple.h
// 递归终止条件
constexpr
mcl_tuple_visitor (size_t, std::tuple<> const&, void*) { return 0; }
// 利用 SFINAE 进行按类型匹配
template
constexpr typename std::enable_if::value, void>::type
mcl_copy (lhs_t* , rhs_t* )
{ throw std::bad_cast (); } // 指定下标元素类型不可隐式转换到目标类型
template
constexpr typename std::enable_if::value, void>::type
mcl_copy (lhs_t* lhs, rhs_t* rhs) noexcept
{ const_cast::type>(*lhs) = *rhs; }
// 运行期访问元组指定下标元素并赋值至目标变量
template void
mcl_tuple_visitor (size_t index, std::tuple 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 const&>(t), ptr);
else mcl_copy (ptr, &std::get<0>(t));
}
使用operator<<
输出时,由于模板不指定目标类型,故需要进行特殊化处理。
#!/mclib/src/pyobj/tuple.h
// 判断目标类型是否可输出,并利用 SFINAE 进行类型匹配
struct mcl_do_is_printable {
template(0) << *static_cast(0))>
static std::integral_constant test(int);
template
static std::integral_constant test(...);
};
template
struct mcl_is_printable
: public mcl_do_is_printable {
typedef decltype(test(0)) type;
};
template
constexpr typename std::enable_if::type::value, void>::type
mcl_oss_print (lhs_t* lhs, rhs_t* rhs) noexcept
{ *lhs << *rhs; }
template
constexpr typename std::enable_if::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
mcl_tuple_printer (size_t, std::tuple<> const&, void*) noexcept{ return 0; }
template void
mcl_tuple_printer (size_t index, std::tuple 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 const&>(t), oss);
else mcl_oss_print (oss, &std::get<0>(t));
}
元组大小比较
Python 中,我们可以直接比较两个元组的大小,其规则是:
如果比较的元素是同类型的,则比较其值,返回结果。
如果两个元素不是同一种类型,则检查它们是否是数字。
- 如果是数字,执行必要的数字强制类型转换,然后比较。
- 如果有一方的元素是数字,则另一方的元素"大"(数字是"最小的")
- 否则,通过类型名字的字母顺序进行比较。
如果有一个列表首先到达末尾,则另一个长一点的列表"大"。
如果我们用尽了两个列表的元素而且所 有元素都是相等的,那么结果就是个平局,就是说返回一个 0。
而对于 C++ 中的 std::tuple
,仅支持类型完全相同的两个元组之间的比较。为实现 python 中元组的比较效果,我们可以通过如下方式实现:
#!/mclib/src/pyobj/tuple.h
// 递归终止条件
template
constexpr typename std::enable_if::type
cmp (std::tuple const& , std::tuple const& ) noexcept
{ return -1; } // lhs 首先达到了末尾,则 rhs 较大
template
constexpr typename std::enable_if::type
cmp (std::tuple const& , std::tuple const& ) noexcept
{ return 1; } // rhs 首先达到了末尾,则 lhs 较大
template
constexpr typename std::enable_if::type
cmp (std::tuple const& , std::tuple const& ) noexcept
{ return 0; } // 同时到达末尾,说明两个元组的内容完全一致(类型可能不同)
template
constexpr typename std::enable_if<
!(std::is_same::value
|| (std::is_arithmetic::value && std::is_arithmetic::value)
), int>::type
cmp (std::tuple const&, std::tuple const&) noexcept {
return std::is_arithmetic::value ? -1
: ( std::is_arithmetic::value ? 1 : (sizeof (lhsf) > sizeof (rhsf)) );
} // 未到达末尾且首个元素类型不同。比较类型,数字类型更小,否则按sizeof进行比较。
template
constexpr typename std::enable_if<
(std::is_same::value
|| (std::is_arithmetic::value && std::is_arithmetic::value)
), int>::type
cmp (std::tuple const& lhs, std::tuple const& rhs) noexcept {
return std::get<0>(lhs) == std::get<0>(rhs) ?
cmp (reinterpret_cast const&>(lhs),
reinterpret_cast const&>(rhs))
: (std::get<0>(lhs) > std::get<0>(rhs)) - (std::get<0>(lhs) < std::get<0>(rhs));
} // 未到达末尾且首个元素类型相同,则比较两个元组的首个元素是否相等。若不相等则返回比较结果,相等则递归比较下一个元素。
元组长度
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::value;
简易对其进行一个封装:
template
constexpr size_t len (std::tuple const&) noexcept
{ return sizeof... (T); }
元组索引
而运行时期按下标进行访问,我们上面已经实现了核心代码,现在只需要创建一个继承自std::tuple
的类,姑且取名叫做pytuple
。
#!/mclib/src/pyobj/tuple.h
template
class pytuple
: public std::tuple {
public:
constexpr pytuple () = default;
constexpr pytuple (const pytuple&) = default;
constexpr pytuple (pytuple&&) = default;
pytuple& operator= (pytuple const&) = default;
pytuple& operator= (pytuple&&) = default;
constexpr pytuple (std::tuple const& tp)
: std::tuple (tp) { }
constexpr pytuple (T&&... parms)
: std::tuple (std::make_tuple (std::forward(parms)...)) { }
};
定义函数maktuple
,以便于创建元组对象。
template
constexpr pytuple::type>::type...>
maktuple (Ts&&... agv) noexcept {
return std::make_tuple (std::forward(agv)...);
}
并重载pytuple::operator[]
,使其接受下标访问即可。
constexpr proxy_t operator[] (long long index) const noexcept
{ return proxy_t (*this, index >= 0 ? index : sizeof...(T) + index); }
这里返回一个pytuple::proxy_t
,用于接收需要转换的类型为参数。
#!/mclib/src/pyobj/tuple.h
template
class proxy_t {
friend pytuple;
friend class pytuple::iterator;
std::tuple const& m_ptr;
size_t index;
constexpr proxy_t (std::tuple const& m, size_t i) noexcept
: m_ptr (m), index (i) { }
public:
template
inline operator cv const () const{
cv v;
mcl_tuple_visitor (index, m_ptr, &v);
return v;
}
friend inline std::ostream&
operator<< (std::ostream& os, proxy_t const& rhs) {
mcl_tuple_printer (rhs.index, rhs.m_ptr, &os);
return os;
}
friend inline std::wostream&
operator<< (std::wostream& os, proxy_t const& rhs) {
mcl_tuple_printer (rhs.index, rhs.m_ptr, &os);
return os;
}
};
元组遍历
在 Python 中,我们可以像这样对元组进行遍历输出:
tu = ('Thank', 'you', 'for', 'making', 'use', 'of')
for i in tu:
print (i)
在 C++ 中也有基于范围的循环。我们只需要提供begin
和end
方法即可利用这一语法糖。
constexpr iterator begin () const noexcept{ return iterator {*this, 0}; }
constexpr iterator end () const noexcept{ return iterator {*this, sizeof...(T)}; }
这里返回一个pytuple::iterator
用于进行索引。
#!/mclib/src/pyobj/tuple.h
class iterator {
std::tuple const& m_ptr;
size_t index;
constexpr iterator (std::tuple const& m, size_t i) noexcept
: m_ptr (m), index (i) { }
friend pytuple;
public:
constexpr pytuple::proxy_t operator* () noexcept
{ return pytuple::proxy_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 << ' ';
如果只是用于输出,可以利用递归来优化。
#!/mclib/src/pyobj/tuple.h
private:
template
constexpr typename std::enable_if<
(i >= sizeof...(T)), std::basic_ostream& >::type
str_ (std::basic_ostream& ss) const noexcept
{ return ss << ")"; }
template
constexpr typename std::enable_if<
(i < sizeof...(T)), std::basic_ostream& >::type
str_ (std::basic_ostream& ss) const noexcept
{ return ss << ", " << std::get(*this), str_(ss); }
public:
friend constexpr std::ostream&
operator<< (std::ostream& os, pytuple 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 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] << '\n'; // m = 99.f
完整代码
点击查看代码
#!/mclib/src/pyobj/tuple.h
/*
mclib (Multi-Canvas Library)
Copyright (C) 2021-2022 Yukino Amamiya
This file is part of the mclib Library. This library is
a graphics library for desktop applications only and it's
only for windows.
This library is free software; you can redistribute it
and/or modify it under the terms of the GNU Library
General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your
option) any later version.
This library is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU Library General Public License for
more details.
You should have received a copy of the GNU Library General
Public License along with this library; if not, write to
the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Yukino Amamiya
iamyukino[at outlook.com]
@file src/pyobj.h
This is a C++11 header.
*/
#ifndef MCL_PYOBJ
# define MCL_PYOBJ
# include
# include
# include
# include
# include
# include
namespace
mcl {
/**
* @class pytuple
* @brief simplified python tuple
*
* @code
* 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::string str = t1[-1];
* std::cout << str << '\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
* @endcode
*
* @ingroup py-style
* @ingroup mclib
* @{
*/
// Runtime subscript access tuples
constexpr
mcl_tuple_visitor (size_t, std::tuple<> const&, void*) { return 0; }
// Recursive termination condition
template
constexpr typename std::enable_if::value, void>::type
mcl_copy (lhs_t* , rhs_t* )
{ throw std::bad_cast (); }
template
constexpr typename std::enable_if::value, void>::type
mcl_copy (lhs_t* lhs, rhs_t* rhs) noexcept
{ const_cast::type>(*lhs) = *rhs; }
// Type matching using SFINAE
template void
mcl_tuple_visitor (size_t index, std::tuple const& t, visit_t* ptr) {
if (index >= (1 + sizeof...(Ts)))
throw std::invalid_argument ("Bad Index");
// The specified subscript element type cannot be
// implicitly converted to the target type
else if (index > 0)
mcl_tuple_visitor (index - 1, reinterpret_cast const&>(t), ptr);
else mcl_copy (ptr, &std::get<0>(t));
}
// Outputs specified tuple elements at run time
struct mcl_do_is_printable {
template(0) << *static_cast(0))>
static std::integral_constant test(int);
template
static std::integral_constant test(...);
};
template
struct mcl_is_printable
: public mcl_do_is_printable {
typedef decltype(test(0)) type;
}; // Judge whether the target type can be output, and use SFINAE for type matching
template
constexpr typename std::enable_if::type::value, void>::type
mcl_oss_print (lhs_t* lhs, rhs_t* rhs) noexcept
{ *lhs << *rhs; }
template
constexpr typename std::enable_if::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
mcl_tuple_printer (size_t, std::tuple<> const&, void*) noexcept{ return 0; }
template void
mcl_tuple_printer (size_t index, std::tuple 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 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
// Compare between tuples of defferent types
template
constexpr typename std::enable_if::type
cmp (std::tuple const& , std::tuple const& ) noexcept
{ return -1; } // lhs reaches the end first, then rhs is larger
template
constexpr typename std::enable_if::type
cmp (std::tuple const& , std::tuple const& ) noexcept
{ return 1; } // rhs reaches the end first, then lhs is larger
template
constexpr typename std::enable_if::type
cmp (std::tuple const& , std::tuple const& ) noexcept
{ return 0; } // lhs and rhs reaches the end at the same time, this means two tuples
// are exactly equal.
template
constexpr typename std::enable_if<
!(std::is_same::value
|| (std::is_arithmetic::value && std::is_arithmetic::value)
), int>::type
cmp (std::tuple const&, std::tuple const&) noexcept {
return std::is_arithmetic::value ? -1
: ( std::is_arithmetic::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
constexpr typename std::enable_if<
(std::is_same::value
|| (std::is_arithmetic::value && std::is_arithmetic::value)
), int>::type
cmp (std::tuple const& lhs, std::tuple const& rhs) noexcept {
return std::get<0>(lhs) == std::get<0>(rhs) ?
cmp (reinterpret_cast const&>(lhs),
reinterpret_cast 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.
// get the length of tuple
template
constexpr size_t len (std::tuple const&) noexcept
{ return sizeof... (T); }
// class like tuple in python
template
class pytuple
: public std::tuple {
public:
template
class proxy_t {
friend pytuple;
friend class pytuple::iterator;
std::tuple const& m_ptr;
size_t index;
constexpr proxy_t (std::tuple const& m, size_t i) noexcept
: m_ptr (m), index (i) { }
public:
template
inline operator cv const () const{
cv v;
mcl_tuple_visitor (index, m_ptr, &v);
return v;
}
friend inline std::ostream&
operator<< (std::ostream& os, proxy_t const& rhs) {
mcl_tuple_printer (rhs.index, rhs.m_ptr, &os);
return os;
}
friend inline std::wostream&
operator<< (std::wostream& os, proxy_t const& rhs) {
mcl_tuple_printer (rhs.index, rhs.m_ptr, &os);
return os;
}
};
public:
constexpr pytuple () = default;
constexpr pytuple (const pytuple&) = default;
constexpr pytuple (pytuple&&) = default;
pytuple& operator= (pytuple const&) = default;
pytuple& operator= (pytuple&&) = default;
constexpr pytuple (std::tuple const& tp)
: std::tuple (tp) { }
constexpr pytuple (T&&... parms)
: std::tuple (std::make_tuple (std::forward(parms)...)) { }
constexpr proxy_t operator[] (long long index) const noexcept
{ return proxy_t (*this, index >= 0 ? index : sizeof...(T) + index); }
public:
template
constexpr bool operator< (proxy_t const& rhs) const noexcept
{ return cmp (*this, rhs) < 0; }
template
constexpr bool operator> (proxy_t const& rhs) const noexcept
{ return cmp (*this, rhs) > 0; }
template
constexpr bool operator== (proxy_t const& rhs) const noexcept
{ return cmp (*this, rhs) == 0; }
template
constexpr bool operator!= (proxy_t const& rhs) const noexcept
{ return cmp (*this, rhs) != 0; }
template
constexpr bool operator<= (proxy_t const& rhs) const noexcept
{ return cmp (*this, rhs) <= 0; }
template
constexpr bool operator>= (proxy_t const& rhs) const noexcept
{ return cmp (*this, rhs) >= 0; }
private:
template
constexpr typename std::enable_if<
(i >= sizeof...(T)), std::basic_ostream& >::type
str_ (std::basic_ostream& ss) const noexcept
{ return ss << ")"; }
template
constexpr typename std::enable_if<
(i < sizeof...(T)), std::basic_ostream& >::type
str_ (std::basic_ostream& ss) const noexcept
{ return ss << ", " << std::get(*this), str_(ss); }
public:
friend constexpr std::ostream&
operator<< (std::ostream& os, pytuple 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 const& tu) {
return sizeof...(T) ? (
os << '(' << std::get<0>(tu),
(sizeof...(T) == 1) ? (os << L",)") : (tu.str_ (os))
) : (os << L"()");
}
public:
class iterator {
std::tuple const& m_ptr;
size_t index;
constexpr iterator (std::tuple const& m, size_t i) noexcept
: m_ptr (m), index (i) { }
friend pytuple;
public:
constexpr pytuple::proxy_t operator* () noexcept
{ return pytuple::proxy_t(m_ptr, index); }
constexpr bool operator!= (iterator const& rhs) const noexcept
{ return this->index != rhs.index; }
inline iterator& operator++ () noexcept{ ++ index; return *this; }
};
constexpr iterator begin () const noexcept{ return iterator {*this, 0}; }
constexpr iterator end () const noexcept{ return iterator {*this, sizeof...(T)}; }
public:
template
constexpr bool operator< (pytuple const& rhs) noexcept
{ return cmp (*this, rhs) < 0; }
template
constexpr bool operator> (pytuple const& rhs) noexcept
{ return cmp (*this, rhs) > 0; }
template
constexpr bool operator== (pytuple const& rhs) noexcept
{ return cmp (*this, rhs) == 0; }
template
constexpr bool operator!= (pytuple const& rhs) noexcept
{ return cmp (*this, rhs) != 0; }
template
constexpr bool operator<= (pytuple const& rhs) noexcept
{ return cmp (*this, rhs) <= 0; }
template
constexpr bool operator>= (pytuple const& rhs) noexcept
{ return cmp (*this, rhs) >= 0; }
};
template
constexpr pytuple::type>::type...>
maktuple (Ts&&... agv) noexcept {
return std::make_tuple (std::forward(agv)...);
}
/** @} */
} // namespace
#endif // MCL_PYOBJ