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++ 中也有基于范围的循环。我们只需要提供beginend方法即可利用这一语法糖。

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