FANG

(C++11)Numeric limits & Type traits

😎Mooc 📚C/C++ 📆 2016年12月31日 📖 364 💬 1

C++ 作为一门强类型的语言,有一套复杂精密的类型系统……

Numeric limits

<limits> 头定义的 numeric_limits 模版类提供了获取算数类型(arithmetic type)值域的标准方式。非算数基本类型,如 std::complex<T> 或者 std::nullptr_t 则没有特化。

cv-qualified 等效于 unqualified。例如std::numeric_limits<const int>std::numeric_limits<volatile int>std::numeric_limits<const volatile int> 等效于 std::numeric_limits<int>

numeric_limits 有一些成员常量,如 is_signed;还有一些成员函数,最重要的是:

  • min():最少极限,能表示的最小正数
  • max():最大极限,能表示的最大值,超过就会溢出
  • lowest():最小极限,能表示的最小值(C++11)

<climits> 定义了一些宏常量,也可以直接得到最大最小值。

  • CHAR_BIT:一个字节的位数
  • MB_LEN_MAX:多字节字符最大字节数
  • CHAR_MIN:char 类型的最小值
  • CHAR_MAX:char 类型的最大值
  • ……

<cstdint> 定义了一些宏常量,也可以直接得到最大最小值。

  • INT8_MIN
  • INT16_MIN
  • INT32_MIN
  • INT64_MIN
  • INT8_MAX
  • INT16_MAX
  • INT32_MAX
  • INT64_MAX
  • ……

Type traits

<type_traits> 头定义了基于模板的,编译时查询或修改类型属性的接口。

#include <iostream>
#include <type_traits>

int main()
{
  std::cout << std::boolalpha;
  std::cout << std::is_void<void>::value << '\n'; // true
  std::cout << std::is_void<const void>::value << '\n'; // true
  std::cout << std::is_void<volatile void>::value << '\n'; // true
  std::cout << std::is_void<volatile const void>::value << '\n'; // true
  std::cout << std::is_void<int>::value << '\n'; // false
}

类似地,还有:

  • is_null_pointer
  • is_integral
  • is_floating_point
  • is_array
  • is_enum
  • is_union
  • is_class
  • is_function
  • is_pointer
  • is_lvalue_reference
  • is_rvalue_reference
  • is_member_object_pointer
  • is_member_function_pointer

💡 从 C++17 开始加入了辅助变量,如

template< class T >
inline constexpr bool is_void_v = is_void<T>::value;

这样我们可以加上 _v 来少敲几次键盘。

更高端更酷炫的,还有判断一个类是否是多态的(Polymorphic)的 std::is_polymorphic,判断一个类是否是抽象的(Abstract)的 std::is_abstract(也即包含纯虚函数的类),判断一个对象是否是 POD(Plain Old Data)类型的 std::is_pod……还有很多很多,就不一一列举了,大家可以参考文末链接。用法是一样的。

对于类,还可以获得它所支持的操作:

  • is_constructible
  • is_default_constructible
  • is_copy_constructible
  • is_move_constructible
  • is_assignable
  • is_copy_assignable
  • is_move_assignable
  • is_destructible

他们各还有 trivially 和 nothrow 版本。trivially construct 简单的理解是 POD 的必要条件。

#include <iostream>
#include <type_traits>
 
class Foo {
    int v1;
    double v2;
 public:
    Foo(int n) : v1(n), v2() {}
    Foo(int n, double f) noexcept : v1(n), v2(f) {}
};
 
int main() {
    std::cout << "Foo is ...\n" << std::boolalpha
              << "\tTrivially-constructible from const Foo&? "
              << std::is_trivially_constructible<Foo, const Foo&>::value << '\n'
              << "\tTrivially-constructible from int? "
              << std::is_trivially_constructible<Foo, int>::value << '\n'
              << "\tConstructible from int? "
              << std::is_constructible<Foo, int>::value << '\n'
              << "\tNothrow-constructible from int? "
              << std::is_nothrow_constructible<Foo, int>::value << '\n'
              << "\tNothrow-constructible from int and double? "
              << std::is_nothrow_constructible<Foo, int, double>::value << '\n';
}

输出:

Foo is ...
Trivially-constructible from const Foo&? true
Trivially-constructible from int? false
Constructible from int? true
Nothrow-constructible from int? false
Nothrow-constructible from int and double? true

还有一些很有用的东西:

  • std::rank:获得数组的维度
  • std::extent:获得数组某个维度的大小
#include <iostream>
#include <type_traits>
 
int main()
{
    std::cout << std::rank_v<int[1][2][3]> << '\n'; // 3
    std::cout << std::rank_v<int[][2][3][4]> << '\n'; // 4
    std::cout << std::rank_v<int> << '\n'; // 0

    std::cout << std::extent_v<int[3]> << '\n'; // 3 < default dimension is 0
    std::cout << std::extent_v<int[3][4], 0> << '\n'; // 3
    std::cout << std::extent_v<int[3][4], 1> << '\n'; // 4
    std::cout << std::extent_v<int[3][4], 2> << '\n'; // 0
    std::cout << std::extent_v<int[]> << '\n'; // 0

    std::cout << std::extent<int[9]>{} << '\n'; // 9 < implicit conversion to std::size_t

    const int ints[] = { 1,2,3,4 };
    std::cout << std::extent_v<decltype(ints)> << '\n'; // 4 < array size
}
  • std::is_same:类型是否相同
#include <iostream>
#include <type_traits>
#include <cstdint>

int main()
{
    std::cout << std::boolalpha
        << std::is_same_v<int, int32_t> << '\n' // true
        << std::is_same_v<int, int64_t> << '\n' // false
        << std::is_same_v<float, int32_t> << '\n' // false
        << std::is_same_v<int, signed int> << "\n" // true
        << std::is_same_v<int, const int> << "\n" // false
        << std::is_same_v<int, volatile int> << "\n" // false
        << std::is_same_v<int, int&> << "\n" // false
        << std::is_same_v<int, int&&> << "\n" // false

        // unlike other types 'char' is not 'unsigned' and not 'signed'
        << std::is_same_v<char, char> << "\n" // true
        << std::is_same_v<char, unsigned char> << "\n" // false
        << std::is_same_v<char, signed char> << "\n" // false
        << std::is_same_v<char, char8_t> << "\n"; // false (since C++20)
}
  • std::is_base_of<Base, Derived>:是否是基类
  • std::is_convertible<From, To>:是否可以转换
#include <iostream>
#include <type_traits>

int main() 
{
    class A {};
    class B : public A {};
    class C {};
 
    bool b2a = std::is_convertible<B*, A*>::value; // true
    bool a2b = std::is_convertible<A*, B*>::value; // false
    bool b2c = std::is_convertible<B*, C*>::value; // false

    std::cout << std::boolalpha;
    std::cout << "a2b: " << std::is_base_of<A, B>::value << '\n'; // true
    std::cout << "b2a: " << std::is_base_of<B, A>::value << '\n'; // false
    std::cout << "c2b: " << std::is_base_of<C, B>::value << '\n'; // false
    std::cout << "same type: " << std::is_base_of<C, C>::value << '\n'; // true
}

此外,还可以修改类型:

  • remove_cv / add_cv
  • remove_const / add_const
  • remove_volatile / add_volatile
  • remove_reference / add_lvalue_reference, add_rvalue_reference
  • remove_pointer / add_pointer
  • make_signed / make_unsigned

以及特殊的:

  • remove_extent:移除数组的某个维度
  • remove_all_extents:移除数组的所有维度
  • result_of:函数返回值的类型
  • ……

更多例子参阅文末链接。

SFINAE

C++ 11 起引入了 SFINAE(替换失败不是错误,Substitution Failure Is Not An Error)的概念。为了说明什么叫“Subsitution”,看下面的例子:

template <
    class T,
    class = typename T::type,  // If not specifed, use default type
> void foo() {
    std::cout << "1\n";
}

template <class T> void foo() {
    std::cout << "2\n";
}

int main() {
    foo<int>(); // 2
    return 0;
}

在调用 foo() 时,编译器会选择恰当的重载函数进行调用。当编译器尝试使用第一个 foo() 时,由于 int 类型没有 type 成员,因此这次“Substitution”失败了。但是我们还有其他路径:第二个 foo() 匹配成功。

但是如下的情况会失败,编译器无法匹配到第二个 foo()

template <typename A>
struct B { typedef typename A::type type; };

template <
    class T,
    class U = typename B<T>::type
> void foo() {
    std::cout << "1\n";
}

template <class T> void foo() {
    std::cout << "2\n";
}

int main() {
    foo<int>(); // error
    return 0;
}

从 C++14 开始,规定替换以词法序进行,因此如下写法可以避免此问题:

template <typename A>
struct B { typedef typename A::type type; };

template <
    class T,
    class = typename T::type,    // Dummy template typename
    class U = typename B<T>::type
> void foo() {
    std::cout << "1\n";
}

template <class T> void foo() {
    std::cout << "2\n";
}

int main() {
    foo<int>(); // 2
    return 0;
}

假设我们有一些类,代表不同的水果,其中有一些需要洗了再吃,还有一些只需要剥皮就可以吃。

struct Fruit {};
struct Apple :Fruit {
    static const bool need_to_wash = true;
};
struct Banana:Fruit {
    static const bool need_to_wash = false;
};

现在我们写一个 eat() 函数,根据不同种类的水果自动选择不同的吃法。在这里,由于我们不知道将来会加入多少水果,简单地根据类型名来重载是不现实的。

利用 SFINAE 特性,标准库提供了 std::enable_if 模板类:

template<bool B, class T = void>
struct enable_if {};

template<class T>
struct enable_if<true, T> { typedef T type; };

template< bool B, class T = void >
using enable_if_t = typename enable_if<B, T>::type;

然后我们就可以解决这个问题了: 😎

template<class T>
void eat(
    T it,
    std::enable_if_t<T::need_to_wash, int> = 0
) {
    std::cout << "wash and eat\n";
}

template<class T>
void eat(
    T it,
    std::enable_if_t<!T::need_to_wash, int> = 0
) {
    std::cout << "eat\n";
}

int main() {
    eat(Apple{});
    eat(Banana{});
    return 0;
}

输出:

wash and eat
eat

参考

第一次编辑于2016/12/31,第二次编辑于2017/11/19,第三次编辑于2020/3/24