Introduction

程序设计需要不断地做抉择,抉择便需用到逻辑分派。

Modern C++ 中,有多种方式完成这个任务,例如 Run-time if,Tag dispatching,SFINAE,Partial Specialization 等等。这些方式分为运行期分派和编译期分派,分派的条件称为约束。设计是为了厉行约束,理想上,能在编译期强制表现的约束就应提升到编译期完成。

C++17 又增加了一种编译期方式 Compile-time if;C++20 中,又增加了 Concepts。

本篇就来总结一下各种方式的用法异同(Concepts见下篇)。

Run-time if

运行期 if 就是运行时期才会执行的分支语句,例如:

template <typename T>
void foo(const T& val)
{
    if(std::is_integral<T>::value) {
        std::cout << "integral\n";
    } else {
        std::cout << "non-integral\n";
    }
}

if-elseswitch 都可以在运行期进行逻辑分派,大部分时候的执行期成本亦微不足道。但是 if-else 要求每一个分支都得编译成功,即使不会执行到的分支,比如返回两个不同类型值的时候,便会出现编译错误。

下面,来看一个贯穿本文的例子。

C++ 有一个 stringstream 字符串流,可以用来读取和写出字符流,我们可以利用它来写一个类型转换工具(C++11 就有一个 std::is_convertible)。

首先,需要判断类型是否可以直接相互转换(包含隐式转换),可以直接转换的则直接转换,不可直接转换的则利用 stringstream 转换。我们使用 convertible 来完成该工作:

template <class T, class U>
class convertible
{
    using small_type = char;
    class big_type { char dummy[2]; };
    static small_type test(T);
    static big_type test(...);
    static U makeU();
public:
    enum { value = sizeof(test(makeU())) == sizeof(small_type) };
};

这里使用了两个稻草人函数 test() 来判断类型是否可以进行转换,两个稻草人函数返回的类型大小不同,如果 U 可隐式转换为 T,则会返回 small_type;若不可转换,则会调用任意参版本,返回 big_type。通过对比两者大小,便能判断两个类型是否能够进行转换。

现在,通过 Run-time if 来完成实际转换:

// Run-time if
template <class T, class U>
void convert(T& to, const U& from)
{
    if(convertible<T, U>::value) {
        to = from;  // error if false!
    } else {
        std::stringstream ss;
        ss << from;
        ss >> to;
    }
}

int main()
{
    int val;
    std::string str{"0706"};
    convert(val, str);

    // output: 
    std::cout << val << std::endl;
}

这里,如果 convertible 的结果为 false,则会编译失败,因为 if-else 的每个分支都要编译通过,即使不会被执行到。

Tag Dispatching

可以使用 tag 来进行编译期流程分派,从而解决 Run-time if 所存在的问题。实现如下:

// Tag dispatching
template <bool v>
struct type_tag { enum { value = v}; };

template <class T, class F>
void convert_impl(T& to, const F& from, type_tag<false>)
{
    std::stringstream ss;
    ss << from;
    ss >> to;
}

template <class T, class F>
void convert_impl(T& to, const F& from, type_tag<true>)
{
    to = from;
}

template <class T, class F>
void convert(T& to, const F& from)
{
    convert_impl(to, from, type_tag<convertible<T, F>::value>());
}

通过 type_tag 将数值转换为型别,由此可以将型别的一个暂时对象传递给重载函数,从而实现编译期分派。测试如下:

int main()
{
    int val;
    std::string str{"0706"};
    double pi = 3.1415;

    convert(val, str);
    std::cout << val << std::endl; // output: 706

    convert(val, pi);
    std::cout << val << std::endl; // output: 3
}

SFINAE

与 Tag-dispatching 密切相关的是 SFINAE,借其实现上述需求,代码如下:

// SFINAE
template <class T, class F,
    typename std::enable_if_t<!convertible<T, F>::value, bool> = true>
void convert(T& to, const F& from)
{
    std::stringstream ss;
    ss << from;
    ss >> to;
}

template <class T, class F,
     typename std::enable_if_t<convertible<T, F>::value, bool> = true>
void convert(T& to, const F& from)
{
    to = from;
}

使用 SFINAE 的效果和 Tag-dispatching 相同,但工作方式不同,Tag-dispatching 使用参数推导来选择合适的 helper 重载,而 SFINAE 直接对主函数操纵重载集。SFINAE 使用起来并不方便,且不易读,有时还需要伴随着一些技巧,不过到了 C++17 以后,我们有更好的武器。

Partial Specialization

另一种方法是直接使用偏特化,通过仿函数来实现逻辑分派。

// Partial specialazation
template <class T, class F, bool> struct convert_functor {};

template <class T, class F>
struct convert_functor<T, F, true>
{
    void operator()(T& to, const F& from) const
    {
        to = from;
    }
};

template <class T, class F>
struct convert_functor<T, F, false>
{
    void operator()(T& to, const F& from) const 
    {
        std::stringstream ss;
        ss << from;
        ss >> to;
    }

    void operator()(std::string& to, const F& from) const
    {
        std::stringstream ss;
        ss << from;
        to = ss.str();
    }
};

template <class T, class F>
void convert(T& to, const F& from)
{
    convert_functor<T, F, convertible<T, F>::value>()(to, from);
}

这种方式最为灵活,但即使只需一个简单二元分派,所需代码相较亦多。

Compile-time if

Compile-Time if 是 C++17 的特性,可以使用它来简单地实现上述需求:

// Compile-time if
template <class T, class F>
void convert(T& to, const F& from)
{
    if constexpr (convertible<T, F>::value) {
        to = from;
    } else {
        std::stringstream ss;
        ss << from;
        ss >> to;
    }
}

与 Run-time if 一样,所有代码都可写于一处,无需像 Tag-dispatching,Partial specialization 那样添加额外的 helper 辅助函数。可以使用 Compile-Time if 的情境下,应该优先考虑使用,以简化代码。但是依旧有一些需要注意的地方。举个例子:

template <typename T>
constexpr auto foo(const T& val)
{
    if constexpr (std::is_integral<T>::value)
    {
        if constexpr (T{} < 10) {
            return val * 2;
        }
    }

    return val;
}

这样使用起来完全没问题:

constexpr auto a = foo(10);   // 20
constexpr auto b = foo("hi"); // hi

而如果合并两个if,结果便不如所愿:

template <typename T>
constexpr auto foo(const T& val)
{
    if constexpr (std::is_integral<T>::value && T{} < 10) 
    {
        return val * 2;
    }

    return val;
}

相同的调用,constexpr auto b = foo("hi"); 会出现编译错误。这是由于 Compile-time if 在条件实例化时需要整个条件都有效,而 T{} < 10 不满足,是以当有多个编译期条件时,应该采用嵌入多个 if 的方式编写代码。

除了 Compile-Time if,C++20 的 Concepts 允许使用简单的语法对模板添加任意 requirements/conditions,下篇来看。

1 thought on “Simplify Code with “if constexpr” in C++17”

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.