问题背景

想要实现一个append(C& contianer, T* ptr, size_t size)函数,作用是拷贝size个数组ptr中的元素到容器container中。为了防止container扩容引发的性能开销,希望如果container有reserve成员,先调用reserve分配好内存,如果没有就算了。

template <typename C, typename T>
void append(C& container, T* ptr, size_t size)
{
    container.reserve(container.size() + size); // 有reserve的版本
    for (size_t i = 0; i < size; ++i) {
        container.push_back(ptr[i]);
    }
}
 
template <typename C, typename T>
void append(C& container, T* ptr, size_t size)
{
    // 无reserve的版本
    for (size_t i = 0; i < size; ++i) {
        container.push_back(ptr[i]);
    }
}

我们能不能像python一样,

template <typename C, typename T>
void append(C& container, T* ptr, size_t size)
{
    if (has_reserve<C>::value) { // 实现一个trait,判断C有没有reserve成员
        container.reserve(container.size() + size); // #1
    }
    for (size_t i = 0; i < size; ++i) {
        container.push_back(ptr[i]);
    }
}

很遗憾不行,因为当类型C没有reserve成员时,#1处直接编译错误。

解决方案

#include <vector>
#include <iostream>
#include <list>
#include <type_traits>
 
using namespace std;
 
 
template <typename T, typename = void_t<>>
struct has_reserve : false_type {};
 
template <typename T>
struct has_reserve<T, void_t<decltype(declval<T&>().reserve(1U))>> : true_type {};
 
template <typename C, typename T>
void append(C& container, T* ptr, size_t size)
{
    if constexpr (has_reserve<C>::value) { // #2
        container.reserve(container.size() + size);
    }
    for (size_t i = 0; i < size; ++i) {
        container.push_back(ptr[i]);
    }
}
 
template <typename T>
void print(const T& container)
{
    cout << __PRETTY_FUNCTION__ << ": ";
    for (auto it = container.begin(); it != container.end(); ++it) {
        cout << *it << ' ';
    }
    cout << endl;
}
 
 
int main()
{
    cout << has_reserve<vector<int>>::value << endl;
    cout << has_reserve<list<int>>::value << endl;
    int a[] = {1,2,3,4,5};
    list<int> lst;
    vector<int> vec;
    // append(lst, data(a), std::size(a));
    // append(vec, data(a), std::size(a));
    append(lst, a, 5);
    append(vec, a, 5);
 
    print(lst); // void print(const T&) [with T = std::__cxx11::list<int>]: 1 2 3 4 5
    print(vec); // void print(const T&) [with T = std::vector<int>]: 1 2 3 4 5
 
    return 0;
}

这里#2处使用了if constexpr功能,由c++17引入(被吴咏炜称为c++17最喜爱的特性)。他的作用是,直接在编译期判断条件,判过了,if下面的语句才会被编译,否则直接跳过。


From AI,

SFINAE(Substitution Failure Is Not An Error)是C++模板编程中的一项关键规则,其核心在于:在函数模板的重载决议过程中,如果模板形参替换导致代码无效,编译器不会视其为错误,而是简单地将该模板特化从候选集中剔除。

工作原理
在模板实例化时,编译器会尝试用具体类型替换模板参数。如果替换后的类型或表达式在”立即语境”(如函数签名、返回类型)中产生非法代码,例如访问不存在的嵌套类型T::type,这次替换失败不会导致编译中止7。编译器会静默放弃这个特化版本,转而尝试其他可能的重载函数4。这一机制使得模板能够根据类型特征在不同实现间进行选择。

关键技术应用
std::enable_if是实现SFINAE的经典工具2。它通过模板特化来控制类型的定义:只有当第一个模板参数为true时,才定义嵌套的type类型3。例如,可以通过std::enable_if_t<std::is_integral_v<T>>来约束模板函数只接受整型参数。

例子

为所有整数类型实现一个特化,

template <typename T>
typename enable_if<!is_integral<T>::value, string>::type
Field2String(const T& field) { return to_string(field); }
 
// specialization for integers
template <typename T>
typename enable_if<is_integral<T>::value, string>::type
Field2String(T field) {
    char buf[32] {0};
    jeaiii::to_text_from_integer(buf, field);
    return buf;
}

为某几个指定的类型实现同一份特化,

// 当模板类型参数为A或B时,匹配这个特化。
template <typename T>
typename std::enable_if<std::is_same<T, A>::value || std::is_same<T, B>::value>::type
Foo(T* item) {
	...
}