C++ 高级元编程(九)
十、重构
模板可以被认为是普通类和函数的一般化。通常,由于新的软件需求,已经测试过的预先存在的函数或类被提升为模板;这通常会节省调试时间。
然而,在添加对应于实现细节的模板参数之前要小心,因为它们将成为类型的一部分。差别不大的对象可能无法互操作。再次考虑 1.4.9 节中的例子,这是一个违反此规则的容器:
template <typename T, size_t INITIAL_CAPACITY = 0>
class special_vector;
让操作符测试任意两个 special_vector 的相等性是有意义的,不管它们的初始容量。
一般来说,所有与额外模板参数正交的成员函数要么需要提升为模板,要么需要移动到基类。 1
事实上,有两种实现是可能的:
- 一个模板函数 special_vector ::operator==对于任意 K 取常量 special_vector &
template <typename T, size_t N>
class special_vector
{
public:
template <size_t K>
bool operator==(const special_vector<T, K>&);
// ...
};
- special_vector 继承自公共 special_vector_base 。这个基类有一个受保护的析构函数和运算符= =(const special _ vector _ base&):
template <typename T>
class special_vector_base
{
public:
bool operator==(const special_vector_base<T>&);
// ...
};
template <typename T, size_t N>
class special_vector : public special_vector_base<T>
{
// ...
};
后一个例子允许更多的灵活性。不应该直接使用基类,但是可以将包装器公开为智能指针/引用,以允许特殊向量(具有相同的 T)的任意集合,而没有意外删除的风险。为了说明这一点,假设您要按如下方式稍微更改代码:
template <typename T>
class pointer_to_special_vector;
template <typename T, size_t N>
class special_vector : private special_vector_base<T>
{
// thanks to private inheritance,
// only the friend class will be able to cast special_vector to
// its base class
friend class pointer_to_special_vector<T>;
};
template <typename T>
class pointer_to_special_vector // <-- visible to users
{
special_vector_base<T>* ptr_; // <-- wrapped type
public:
template <size_t K>
pointer_to_special_vector(special_vector<T,K>* b = 0)
: ptr_(b)
{}
// fictitious code...
T at(size_t i) const { return (*ptr_)[i]; }
};
int main()
{
std::list< pointer_to_special_vector<double> > lp;
special_vector<double, 10> sv1;
special_vector<double, 20> sv2;
lp.push_back(&sv1);
lp.push_back(&sv2); // ok, even if sv1 and sv2 have different static types
}
10.1.向后兼容性
一个典型的重构问题包括修改一个现有的例程,这样任何调用者都可以选择原始的行为或变体。
从一个非常简单的例子开始,假设您想要(可选地)记录每个数字的平方,并且您不想重复代码。所以,你可以修改经典函数模板 sq :
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return x*x;
}
template <typename scalar_t, typename logger_t>
inline scalar_t sq(const scalar_t& x, logger_t logger)
{
// we shall find an implementation for this...
}
struct log_to_cout
{
template <typename scalar_t>
void operator()(scalar_t x, scalar_t xsq) const
{
std::cout << "the square of " << x << " is " << xsq;
}
};
double x = sq(3.14); // not logged
double y = sq(6.28, log_to_cout()); // logged
用户将打开日志,向 sq 的两个参数版本传递一个定制的仿函数。但是在旧功能上实现新功能有不同的方法:
- 封装 :在 sq(scalar_t,logger_t)内部调用 sq(scalar_t)。此解决方案的实施风险极小。
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return x*x;
}
template <typename scalar_t, typename logger_t>
inline scalar_t sq(const scalar_t& x, logger_t logger)
{
const scalar_t result = sq(x);
logger(x, result);
return result;
}
- 接口适配 :转换 sq(scalar_t)以便用无操作记录器秘密调用 sq(scalar_t,logger_t)。这是最灵活的解决方案。 2
struct dont_log_at_all
{
template <typename scalar_t>
void operator()(scalar_t, scalar_t) const
{
}
}
template <typename scalar_t, typename logger_t>
inline scalar_t sq(const scalar_t& x, logger_t logger)
{
const scalar_t result = x*x; // the computation is performed here
logger(x, result);
return result;
}
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return sq(x, dont_log_at_all());
}
- 内核宏 :在算法核心极其简单,需要在静态和动态代码之间共享时工作。
#define MXT_M_SQ(x) ((x)*(x))
template <typename scalar_t>
inline scalar_t sq(const scalar_t& x)
{
return MXT_M_SQ(x);
}
template <typename int_t, int_t VALUE>
struct static_sq
{
static const int_t result = MXT_M_SQ(VALUE);
};
注意内核宏的使用将被 C++0x 关键字 constexpr 取代。
square/logging 的例子很简单,但是令人遗憾的是,代码重复很常见。在许多 STL 实现中,std::sort 被写了两次:
template <typename RandomAccessIter>
void sort(RandomAccessIter __first, RandomAccessIter __last);
template <class RandomAccessIter, typename Compare>
void sort(RandomAccessIter __first, RandomAccessIter __last, Compare less);
使用接口适配,第一个版本是第二个版本的特例:
struct weak_less_compare
{
template <typename T1, typename T2>
bool operator()(const T1& lhs, const T2& rhs) const
{
return lhs < rhs;
}
};
template <typename RandomAccessIter>
void sort(RandomAccessIter __first, RandomAccessIter __last)
{
return sort(__first, __last, weak_less_compare());
}
10.2.重构策略
本节考虑一个示例问题,并揭示一些不同的技术。
10.2.1.用接口重构
预先存在的 private_ptr 类在 void*中保存 malloc 的结果,并在析构函数中释放内存块:
class private_ptr
{
void* mem_;
public:
~private_ptr() { free(mem_); }
private_ptr() : mem_(0)
{ }
explicit private_ptr(size_t size) : mem_(malloc(size))
{ }
void* c_ptr() { return mem_; }
//...
};
现在,您需要扩展该类,以便它可以保存一个指针,指向一个 malloc 块或一个 t 类型的新对象。
由于 private_ptr 负责分配,您可以引入一个具有合适虚函数的私有接口,创建一个单独的派生(模板)类,并让 private_ptr 进行正确的调用:
class private_ptr_interface
{
public:
virtual void* c_ptr() = 0;
virtual ~private_ptr_interface() = 0;
};
template <typename T>
class private_ptr_object : public private_ptr_interface
{
T member_;
public:
private_ptr_object(const T& x)
: member_(x)
{
}
virtual void* c_ptr()
{
return &member_;
}
virtual ~private_ptr_object()
{
}
};
template < >
class private_ptr_object<void*> : public private_ptr_interface
{
void* member_;
public:
private_ptr_object(void* x)
: member_(x)
{
}
virtual void* c_ptr()
{
return member_;
}
virtual ~private_ptr_object()
{
free(member_);
}
};
class private_ptr
{
private_ptr_interface* mem_;
public:
~private_ptr()
{
delete mem_;
}
private_ptr()
: mem_(0)
{
}
explicit private_ptr(size_t size)
: mem_(new private_ptr_object<void*>(malloc(size)))
{
}
template <typename T>
explicit private_ptr(const T& x)
: mem_(new private_ptr_object<T>(x))
{
}
void* c_ptr()
{
return mem_->c_ptr();
}
//...
};
注意虚函数调用在 private_ptr 之外是不可见的。 3
10.2.2.用蹦床重构
前一种方法使用两个分配来存储 void*:一个用于内存块,一个用于辅助 private_ptr_object。蹦床可以做得更好:
template <typename T>
struct private_ptr_traits
{
static void del(void* ptr)
{
delete static_cast<T*>(ptr);
}
};
template <typename T>
struct private_ptr_traits<T []>
{
static void del(void* ptr)
{
delete [] static_cast<T*>(ptr);
}
};
template < >
struct private_ptr_traits<void*>
{
static void del(void* ptr)
{
free(ptr);
}
};
template < >
struct private_ptr_traits<void>
{
static void del(void*)
{
}
};
class private_ptr
{
typedef void (*delete_t)(void*);
delete_t del_;
void* mem_;
public:
~private_ptr()
{
del_(mem_);
}
private_ptr()
: mem_(0), del_(&private_ptr_traits<void>::del)
{
}
explicit private_ptr(size_t size)
{
mem_ = malloc(size);
del_ = &private_ptr_traits<void*>::del;
}
template <typename T>
explicit private_ptr(const T& x)
{
mem_ = new T(x);
del_ = &private_ptr_traits<T>::del;
}
template <typename T>
explicit private_ptr(const T* x, size_t n)
{
mem_ = x;
del_ = &private_ptr_traits<T []>::del;
}
void* c_ptr()
{
return mem_;
}
//...
};
10.2.3.用访问器重构
假设您有处理一系列简单对象的算法:
struct stock_price
{
double price;
time_t date;
};
template <typename iterator_t>
double computePriceIncrease(iterator_t begin, iterator_t end)
{
return ((end-1)->price - begin->price)
/ std::difftime(begin->date, (end-1)->date) * (24*60*60);
}
可能需要重构来处理来自两个独立容器的数据:
std::vector<double> prices;
std::vector<time_t> dates;
// problem: we cannot call computePriceIncrease
对于新的算法 I/O,您有几种选择:
- 假设迭代器指向 pair,其中第一个是价格,第二个是日期(换句话说,写 end->first - begin->first...).如前所述,这通常不是一个好的风格选择。
- 明确提及开始->价格和开始->日期(如前所示)。算法不依赖于迭代器,但是底层类型被约束在 stock_price 的接口上。
- 传递两个不相交的范围。该解决方案的复杂性可能会有所不同。
template <typename I1, typename I2>
double computePriceIncrease(I1 price_begin, I1 price_end, I2 date_begin, I2 date_end)
{
// the code must be robust and handle ranges of different length, etc.
}
- 传递一个范围和两个访问器。
template <typename I, typename price_t, typename date_t>
double computePriceIncrease(I begin, I end, price_t PRICE, date_t DATE)
{
double p = PRICE(*begin);
time_t t = DATE(*begin);
//...
}
struct price_accessor
{
double operator()(const stock_price& x) const
{
return x.price;
}
};
struct date_accessor
{
time_t operator()(const stock_price& x) const
{
return x.date;
}
};
computePriceIncrease(begin, end, price_accessor(), date_accessor());
请注意,您可以欺骗访问器查看其他地方,例如在成员变量中:
struct price_accessor_ex
{
const std::vector<double>& v_;
double operator()(const int x) const
{
return v_[x];
}
};
struct date_accessor_ex
{
const std::vector<time_t>& v_;
time_t operator()(const int x) const
{
return v_[x];
}
};
int main()
{
std::vector<double> prices;
std::vector<time_t> dates;
// ...
assert(prices.size() == dates.size());
std::vector<int> index(prices.size());
for (int i=0; i<prices.size(); ++i)
index[i] = i;
price_accessor_ex PRICE = { prices };
date_accessor_ex DATE = { dates };
computePriceIncrease(index.begin(), index.end(), PRICE, DATE);
}
访问器可能携带对外部容器的引用,所以它们选择从实际参数中推导出的元素。在某些特殊情况下,可以使用指针来避免创建索引容器。但是,使用这种方法时应该非常小心。
// warning: this code is fragile:
// changing a reference to a copy may introduce subtle bugs
struct price_accessor_ex
{
double operator()(const double& x) const
{
return x;
}
};
struct date_accessor_ex
{
const double* first_price_;
size_t length_;
const time_t* first_date_;
time_t operator()(const double& x) const
{
if ((&x >= first_price_) && (&x < first_price_+length_))
return first_date_[&x - first_price_];
else
throw std::runtime_error("invalid reference");
}
};
int main()
{
price_accessor_ex PRICE;
date_accessor_ex DATE = { &prices.front(), prices.size(), &dates.front() };
computePriceIncrease(prices.begin(), prices.end(), PRICE, DATE);
}
该算法引用一个价格,并相应地推导出相应的日期。
10.3.占位符
每个 C++ 对象都可以执行一些动作。空对象,比如 instance_of,可以执行元操作,比如声明它们的类型并将它们的类型“绑定”到模板参数或特定的函数重载。
有时,TMP 的工作是通过用一个相似的空对象替换一个对象,用一个相应的元动作替换一个动作,来阻止工作被完成。
如果 P < T >是一个类,它的公共接口满足与 T 相同的前置和后置条件,但运行时开销最小,则 P 类型被称为 T 的占位符 。在最有利的情况下,它什么也不做。
10.3.1.关闭
关闭是一种算法重构技术,它允许你有选择地“关闭”一些特性,而不需要重写或复制函数。这个名字来源于这样一种典型情况:函数通过引用获取一个对象,在执行过程中被“触发”,最终返回一个独立的结果,这是执行的副产品。该对象可以是在执行期间接收信息的容器或同步对象。
void say_hello_world_in(std::ostream& out)
{
out << "hello world";
}
double read_from_database(mutex& s)
{
// acquire the mutex, return a value from the DB, and release the mutex
}
以最少的代码返工获得不同结果的一种快速而优雅的方法是提供一个具有简化接口的空心对象,特别是不需要任何动态存储。循序渐进:
- 重命名原始函数并将参数升级为模板类型:
template <typename T>
void basic_say_hello_world_in(T& o)
- 添加一个恢复原始行为的霸王:
inline void say_hello_world_in(std::stream& o)
{
return basic_say_hello_world_in(o);
}
- 最后,提供一个“中和”大部分努力的对象:
struct null_ostream
{
template <typename T>
null_ostream& operator<<(const T&)
{
return *this;
}
};
inline void say_hello_world_in()
{
null_stream ns;
basic_say_hello_world_in(ns);
}
关闭习惯用法需要准确了解主算法中使用的对象接口(的子集)。
当你设计一个定制容器时,偶尔添加一个额外的模板参数来启用一个空心模式可能是有用的。您获取原始类并将其提升为模板:
| 类自旋锁{S7-1200 可编程控制器:ptr_t vptr_t 可变类型定义:公共:spinlock(vptr_tconst):bool try _ acquire();bool acquire();/ ... →。}; | 模板类自旋锁;模板< >类旋转锁{S7-1200 可编程控制器:ptr_t vptr_t 可变类型定义:公共:spinlock(vptr_t const):bool try _ acquire();bool acquire();// ...};模板< >类旋转锁{//空心实现 spinlock(请参阅*){}bool try_acquire(){返回 true}布尔获取(){返回 true}//...}; |
如果该类是一个模板,您将需要再添加一个布尔参数。
当然,接口复制的关键点是空心类的一组谨慎但有意义的默认答案,前提是这种复制是可能的(见下面的反例)。这也允许您识别被认为“有效”的对象的最小接口。对象的接口是由它的用途定义的。
最后,您可以将程序限制为自旋锁(可能是“开”或“关”):
template <typename ..., bool IS_LOCKING_REQURED>
void run_simulation(..., spinlock<IS_LOCKING_REQURED>& spin)
{
if (spin.acquire())
{
//...
}
}
或未指定类型的对象,其接口被隐式假定为与自旋锁兼容:
template <typename ..., typename lock_t>
void run_simulation(..., lock_t& lock)
{
if (lock.acquire())
{
//...
}
}
两种选择都是有效的,但在某些情况下,更倾向于选择其中一种(详见第 5.2 节)。
另一个应用是双联减速。有些算法一次处理一个或两个项目,同时对两个项目执行相同的操作。为了避免重复,您需要一个接受一个或两个参数的算法实现。
原型例子是排序两个“同步”数组和矩阵行缩减。由于高斯的原因,该算法对矩阵 M 执行一系列初等运算,并将其转变为对角(或三角形)形式。如果在单位矩阵上并行应用相同的运算,它也获得 M. 4 的逆
因此,您可以编写一个通用函数,使总是接受两个不同静态类型的矩阵,并将它们视为相同:
template <typename matrix1_t, typename matrix2_t>
void row_reduction(matrix1_t& matr, matrix2_t& twin)
{
// ...
for (size_t k=i+1; k<ncols && pivot!=0; ++k)
{
matr(j, k) -= pivot*matr(i, k);
twin(j, k) -= pivot*twin(i, k);
}
// ...
}
假设你已经有一个矩阵类: 5
template <typename scalar_t>
class matrix
{
public:
typedef scalar_t value_type;
size_t rows() const;
size_t cols() const;
void swap_rows(const size_t i, const size_t j);
value_type& operator()(size_t i, size_t j);
value_type operator()(size_t i, size_t j) const;
};
按照中空模式的习惯用法来扩展它是不可能的,因为对于返回引用的函数没有满意的默认答案: 6
template <typename scalar_t, bool NO_STORAGE = false>
class matrix;
template <typename scalar_t>
class matrix<scalar_t, false>
{
/* put the usual implementation here */
};
template <typename scalar_t>
class matrix<scalar_t, true>
{
public:
value_type& operator()(size_t i, size_t j)
{
return /* what? */
}
//...
};
因此,您完全删除引用,并向下移动一级。你中和了容器和被包含的物体。孪生矩阵是在幻影标量上定义的容器;操作符什么也不做的类:
template <typename T>
struct ghost
{
// all operators return *this
ghost& operator-=(ghost)
{
return *this;
}
//...
};
template <typename T>
inline ghost operator*(T, ghost g) { return g; }
template <typename T>
inline ghost operator*(ghost g, T) { return g; }
template <typename scalar_t>
class matrix<scalar_t, true>
{
size_t r_;
size_t c_;
public:
typedef ghost<scalar_t> value_type;
size_t rows() const { return r_; }
size_t cols() const { return c_; }
void swap_rows(const size_t, const size_t) {}
value_type operator()(size_t i, size_t j)
{
return value_type();
}
const value_type operator()(size_t i, size_t j) const
{
return value_type();
}
};
ghost 将是一个无状态类,这样每个操作都是无操作的。特别是,line twin(j,k) -= pivot*twin(i,k)转化为一系列无操作的函数调用。
在这一点上需要更多的细节。
10.3.2.鬼魂
没有真正令人满意的方法来编写虚标量。大多数实现都是半正确的,但是它们可能会有令人讨厌的副作用:
- 如果不对名称空间进行适当的约束,幽灵很可能会在名称空间中出没。因为它们的接口应该支持几乎所有的 C++ 操作符,所以您可能需要编写一些全局操作符,并且您希望确保这些操作符仅在必要时出现。
- 鬼的主要目的是阻止工作被做。如果 G 是鬼,那么 G*3+7 应该编译,什么都不做。很容易获得编译的实现,但是错误地做了一些工作——比如说,因为 G 被转换为整数 0。
ghost 应该是一个模仿其模板参数 T 的类模板,它驻留在一个不同的名称空间中。为了简单起见,可以假设 T 是一个内置的数值类型,这样就可以实现所有可能的操作符。
template <typename T>
struct ghost
{
ghost(T) {}
ghost() {}
//...
};
对于 coherence,比较运算符返回的结果与 ghost 是单态(所有 ghost 都是等效的)这一事实相符,因此运算符
通常,大多数算术运算符都可以用合适的宏来定义: 7
#define mxt_GHOST_ASSIGNMENT(OP) \
ghost& operator OP##= (const ghost) { return *this; }
#define mxt_GHOST_UNARY(OP) \
ghost operator OP() const { return *this; }
#define mxt_GHOST_INCREMENT(OP) \
ghost& operator OP () { return *this; } \
const ghost operator OP (int) { return *this; }
template <typename T>
struct ghost
{
ghost(const T&){}
ghost() {}
mxt_GHOST_INCREMENT(++); // defines pre- and post-increment
mxt_GHOST_INCREMENT(--);
mxt_GHOST_ASSIGNMENT(+); // defines operator+=
mxt_GHOST_ASSIGNMENT(-);
// ...
mxt_GHOST_UNARY(+);
mxt_GHOST_UNARY(-);
//...
};
对于算术/比较运算符,您需要研究这些可能性:
- 带参数 ghost 的成员运算符。
- 带参数 t 的成员运算符。
- 带有参数 const X&的模板成员运算符,其中 X 是一个独立的模板参数。
- 非成员运算符,如
template <typename T>
ghost<T> operator+(ghost<T>, ghost<T>) // variant #1
template <typename T>
ghost<T> operator+(T, ghost<T>) // variant #2
template <typename T1, typename T2>
<???> operator+(ghost<T1>, ghost<T2>) // variant #3
template <typename T1, typename T2>
<???> operator+(T1, ghost<T2>) // variant #4
每个选择都有一些问题。
- 成员运算符将在右侧执行参数提升,但模板全局运算符要求参数推导完全匹配。 8 使用成员操作符 ghost:·operator+(ghost)const,ghost < T > + X 形式的任何和都将成功,只要有可能从 X 构建临时 ghost < T >(因为 ghost 构造函数不是显式的)。但是 X + ghost < T >不会编译。
- 当 T 是一个数字类型(比如 double)并且 X 是一个文字零时,这个问题最明显。成员运算符+将处理 ghost + 0,因为 0 (int) → 0.0 (double) → ghost ,但是 0 + ghost 必须由一个签名不能太严格的全局运算符处理,因为 0 不是 double。
- 这意味着在这种情况下,只有变量#4 是可行的,因为没有其他操作符会完全匹配(int,ghost )。
- 但是,您希望运算符匹配尽可能多的类型,而不是更多。虽然您应该能够编写 int + ghost ,但是您不想接受任何东西。
ghost<double> g;
g + 0; // should work
0 + g; // should work
std::cout + g; // should not work!
g + std::cout; // should not work!
通常,全局操作符应该将执行委托给成员函数:
template <typename T1, typename T2>
inline ghost<T2> operator+ (T1 x, const ghost<T2> y)
{
return y + x;
}
y + x 确实是对任何成员 operator+的调用,所以你可以把接受 T1 作为参数的责任传递给 ghost 自己的接口(编译器会尝试任何重载的 operator+)。
要使赋值合法,转换运算符是必需的:
operator T() const
{
return T();
}
ghost<double> g = 3.14;
double x = g; // error: cannot convert from ghost to double
相反,使用转换运算符和糟糕的运算符实现,无害的代码会突然变得模糊不清:
ghost<double> g;
g + 3.14;
例如,以下各项之间可能存在歧义:
- 3.14 晋升为幽灵,之后是幽灵:::操作员+(幽灵)。
- 将 g 转换为 double,然后进行普通求和。
由于两条路径的等级相等,编译器将会放弃。
在不同的情况下,转换会被意外地调用:
ghost<double> g = 3.14;
double x = 3*g + 7;
编译器应将该代码翻译成以下序列:
double x = (double)(operator*(3, g).operator+(ghost<double>(7)));
如果全局操作符*由于某种原因不能被调用(比方说,它期望 double,ghost ,所以它不会匹配),代码仍然有效,但是它静默地执行一些不同的东西:
double x = 3*(double)(g) + 7;
这需要在运行时进行两次浮点运算,因此它违背了 ghost 的目的。 9
总而言之,在最佳实现中:
- ghost 构造函数是强类型的,所以它需要一个可转换为 t 的参数。
- 您需要成员和非成员操作员:
- 成员操作符将接受任何参数(任何类型 X)并用静态断言(使用构造函数本身)检查 X。
- 非成员操作符会盲目地将任何事情委托给成员函数。
这里描述的是一个不使用宏的实现。无论如何,由同一个预处理器指令生成的函数已经被分组:
#define mxt_GHOST_GUARD(x) sizeof(ghost<T>(x))
template <typename T>
struct ghost
{
ghost(const T&) {}
ghost() {}
operator T() const
{
return T();
}
ghost& operator++ () { return *this; }
const ghost operator++ (int) { return *this; }
ghost& operator-- () { return *this; }
const ghost operator-- (int) { return *this; }
template <typename X> ghost& operator+= (const X& x)
{ mxt_GHOST_GUARD(x); return *this; }
template <typename X> ghost& operator-= (const X& x)
{ mxt_GHOST_GUARD(x); return *this; }
template <typename X> ghost operator+ (const X& x) const
{ mxt_GHOST_GUARD(x); return *this; }
template <typename X> ghost operator- (const X& x) const
{ mxt_GHOST_GUARD(x); return *this; }
template <typename X> bool operator== (const X& x) const
{ mxt_GHOST_GUARD(x); return true; }
template <typename X> bool operator!= (const X& x) const
{ mxt_GHOST_GUARD(x); return false; }
ghost operator+() const { return *this; }
ghost operator-() const { return *this; }
};
template <typename X, typename Y>
ghost<Y> operator+ (const X& x, const ghost<Y> y) { return y + x; }
template <typename X, typename Y>
ghost<Y> operator- (const X& x, const ghost<Y> y) { return -(y - x); }
template <typename X, typename Y>
bool operator== (const X& x, const ghost<Y> y) { return y == x; }
template <typename X, typename Y>
bool operator!= (const X& x, const ghost<Y> y) { return y != x; }
关于 STL 分配器,也有类似的争论。“两个同类容器相等”的概念显然要求元素序列相等,但不清楚这是否也足够。
2 虽然封装向用户传达了一种“开销感”,但接口适配表明新的 sq 要好得多,可以自由使用。
3 换句话说,代码的调用方不用担心继承问题。它们可以通过任何 T,类会自动地自动包装它。这个想法在肖恩·帕伦特的一次演讲中得到了进一步的发展,可以从这个链接免费下载:channel 9 . msdn . com/Events/going native/2013/Inheritance-Is-The-Base-Class-of-Evil。
一个对数学不感兴趣的读者可能想考虑一个类似的情况:软件执行一系列动作,同时记录一系列“撤销”步骤。
5 为了便于算法,数据结构的接口经常被改造。这一课是 STL 设计的里程碑之一。
6 一般来说,空心容器本身没有记忆。您可能会反对,在这里您可以使用单个 scalar_t 数据成员,并为任何一对索引返回对同一对象的引用,但是这种策略会消耗大量 CPU 运行时间,毫无意义地覆盖相同的内存位置。
7 介意使用令牌串联#。您可能想编写 operator ## OP 来连接 operator and +,但这是非法的,因为在 C++ 中,operator 和+是两个不同的标记。另一方面,在+和=之间需要# 来生成运算符+=,所以需要写运算符 OP ## =。
8 将 T 转换为 ghost < T >的自定义构造函数只有在模板实参推演后才被考虑。注意,这里的构造函数甚至不是显式的。参见[2]第 B.2 节。
9 提示:一定要在转换运算符中留一个断点。
十一、调试模板
由于 TMP 代码诱导编译器执行计算,所以实际上不可能一步一步地遵循它。然而,有一些技巧可以帮助你。这一章实际上包含了一些建议和调试策略。
11.1.识别类型
现代的调试器总是会在程序停止时显示变量的确切类型。此外,在调用堆栈中可以看到许多关于类型的信息,其中(成员)函数通常显示有它们的模板参数的完整列表。但是,您经常需要检查中间结果和返回类型。
以下函数有助于:
template <typename T>
void identify(T, const char* msg = 0)
{
std::cout << (msg ? msg : "") << typeid(T).name() << std::endl;
}
记住 type_info:: name 对返回字符串的可读性没有任何保证。 1 使用自由函数返回 void 可以很容易地在调试和优化版本之间切换,因为代码可以简单地使用预处理器指令来替换函数,比如用一个空宏。然而,当你需要识别一个类成员时,这种方法不起作用,比如当你调试 lambda 表达式时。(参见第 9.2 节)。您可能希望检查返回类型是否被正确地推导出来;最佳解决方案是添加一个小型公共数据成员:
template <typename X1, typename F, typename X2>
class lambda_binary : public lambda< lambda_binary<X1,F,X2> >
{
// ...
typedef typename
deduce_argument
<
typename X1::argument_type,
typename X2::argument_type
>::type
argument_type;
#ifdef MXT_DEBUG
instance_of<result_type> RESULT_;
#endif
result_type operator()(argument_type x1, argument_type x2) const
{
identify(RESULT_);
return f_(x1_(x1, x2), x2_(x1, x2));
}
};
添加数据成员特别有用,因为交互式调试器允许您检查内存中的对象并显示它们的确切类型。
一般来说,每当元函数编译但给出错误结果时,添加 instance_of 和 static_value 类型的成员来检查计算的中间步骤,然后在堆栈上创建元函数的本地实例。
template <size_t N>
struct fibonacci
{
static const size_t value = fibonacci<N-1>::value + fibonacci<N-2>::value;
static_value<size_t, value> value_;
fibonacci<N-1> prev1_;
fibonacci<N-2> prev2_;
};
int main()
{
fibonacci<12> F;
}
然后在调试器里看 F。您可以从常量的类型来检查它们。 2
11.1.1.陷印类型
有时在大型项目中,会检测到错误的模式。当这种情况发生时,您需要列出所有使用坏模式的代码行。您可以使用模板创建不编译的函数陷阱,并将它们注入到错误模式中,这样编译器日志将指向您正在寻找的所有行。
假设您发现一个 std::string 被传递给了 printf,并且您怀疑这在项目中发生了几次。
std::string name = "John Wayne";
printf("Hello %s", name); // should be: name.c_str()
class Foo{};
printf("I am %s", Foo());
遍历 printf 的所有实例会花费太多时间,所以您可以在一个公共的包含文件中添加一些陷阱代码。请注意,您必须编写一个始终为假的静态断言,但它依赖于一个未指定的参数 t,在下面的代码中,MXT_ASSERT 是一个静态断言:
template <typename T>
void validate(T, void*)
{
}
template <typename T>
void validate(T, std::string*)
{
MXT_ASSERT(sizeof(T)==0); // if this triggers, someone is passing
// std::string to printf!
}
template <typename T>
void validate(T x)
{
validate(x, &x);
}
template <typename T1>
void printf_trap(const char* s, T1 a)
{
validate(a);
}
template <typename T1, typename T2>
void printf_trap(const char* s, T1 a, T2 b)
{
validate(b);
printf_trap(s, a);
}
template <typename T1, typename T2, typename T3>
void printf_trap(const char* s, T1 a, T2 b, T3 c)
{
validate(c);
printf_trap(s, a, b);
}
// ...
#define printf printf_trap
每次将字符串传递给 printf 时,这个陷阱代码都会导致编译器错误。
能够提到 std::string(在 validate 中)很重要,所以前面的文件必须包含。但是如果您正在测试一个用户类,这可能是不可行的(包括可能导致循环的项目标题),所以您只需用一个通用的 SFINAE 静态断言来替换显式验证测试:
template <typename T>
void validate(T, void*)
{
MXT_ASSERT(!is_class<T>::value); // don't pass classes to printf;
}
11.1.2.不完整的类型
类模板可能不要求 T 是完整的类型。这个需求通常是不明确的,它依赖于内部模板实现的细节。
STL 容器,比如 vector、list 和 set,可以被实现为接受不完整的类型,因为它们动态地分配存储。判定 T 是否不完全的一个充要条件是把它自身的一个容器放入一个类中。
struct S1
{
double x;
std::vector<S1> v;
};
struct S2
{
double x;
std::list<S2> l;
};
特别是,分配器不应该假设 T 已经完成;否则,它可能与标准容器不兼容。
只需向编译器询问类型的大小,就可以轻松获得静态断言:
template <typename T>
struct must_be_complete
{
static const size_t value = sizeof(T);
};
struct S3
{
double x;
must_be_complete<S3> m;
};
test.cpp: error C2027: use of undefined type 'S3'
该技术用于实现安全删除。指向不完整类型的指针可能会被删除,但这会导致未定义的行为(在最好的情况下,T 的析构函数不会被执行)。
template <typename T>
void safe_delete(T* p)
{
typedef T must_be_complete;
sizeof(must_be_complete);
delete x;
}
确定一个模板是否会得到一个完整的类型作为参数可能并不容易。
标准分配器有一个 rebind 成员,允许任何分配器创建分配器,不同的实现将利用这个特性来构造它们自己的私有数据结构。一个容器,比如 std::list ,可能需要分配器>这个类可能不完整。
template <typename T>
class allocator
{
typedef T* pointer;
template <typename other_t>
struct rebind
{
typedef allocator<other_t> other;
};
// ...
};
template <typename T, typename allocator_t>
struct list
{
struct node;
friend struct node;
typedef typename allocator_t::template rebind<node>::other::pointer node_pointer;
// the line above uses allocator<node> when node is still incomplete
struct node
{
node(node_pointer ptr)
{
}
};
// ...
};
要编译节点构造函数,需要 node_pointer。所以编译器查看 allocator ::rebind :“其他”,实际上是 allocator 。
假设您现在有一个高效的类来管理固定长度 N 的内存块:
template <size_t N>
class pool;
为了在一般的无状态分配器中正确地包装它,您可能想写:
template <typename T>
class pool_allocator
{
static pool<sizeof(T)>& get_storage();
// ...
};
但是在这种情况下,类级别的 sizeof(T)的存在要求 T 是完整的。相反,您可以切换到带有模板成员函数的惰性实例化方案:
template <typename T>
class pool_allocator
{
template <typename X>
static pool<sizeof(X)>& get_storage()
{
static pool<sizeof(X)>* p = new pool<sizeof(X)>;
return *p;
}
// ...
void deallocate(pointer ptr, size_type)
{
get_storage<T>().release(ptr);
}
};
现在在类的层面上,sizeof(T)是从来不提的。
注如[7]第 10.14 节所述,堆栈和堆分配是有区别的:
static T& get1()
{
static T x;
return x;
}
static T& get2()
{
static T& x = *new T;
return x;
}
前者会在程序结束的某个未指明的时刻销毁 x,而后者永远不会销毁 x。
所以,如果 T::~T()释放了一个资源,比如说一个互斥体,那么第一个版本就是正确的。但是,如果另一个全局对象的析构函数调用 get1(),可能是 x 已经被销毁了(这个问题被称为“静态初始化顺序惨败”)。
11.1.3.标记全局变量
非类型模板参数可以是指向具有外部链接的对象的任意指针。限制是这个指针不能在编译时被解引用:
template <int* P>
struct arg
{
arg()
{
myMember = *P; // dereference at runtime
}
int myMember;
};
extern int I;
int I = 9;
arg<&I> A;
相反,写下以下内容是非法的:
template <int* P>
struct arg : static_value<int, *P> // dereference at compile time
您可以使用指针将一些元数据与全局常量相关联:
// metadata.hpp
template <typename T, T* global>
struct metadata
{
static const char* name;
};
#define DECLARE_CPP_GLOBAL(TYPE, NAME) \
TYPE NAME; \
template <> const char* metadata<TYPE, &NAME>::name = #NAME
// main.cpp
#include "metadata.hpp"
DECLARE_CPP_GLOBAL(double, xyz);
int main()
{
printf(metadata<double, &xyz>::name); // prints "xyz"
}
11.2.整数计算
本节快速回顾静态整数计算可能导致的一些问题。
11.2.1.有符号和无符号类型
当 T 是整数类型时,T(-1)、-T(1)、T()-1 和~T()之间的差异可能会导致常见问题。
- 如果 T 是无符号的和大的,它们都是相同的。
- 如果 T 是有符号的,前三个是相同的。
- 如果 T 是无符号的并且很小,第二个和第三个表达式可能会给出意外的结果。
我们借用 is_signed_integer 的实现中的一个函数(见 4.3.2 节)。
template <typename T>
static selector<(T(0) > T(-1))> decide_signed(static_value<T, 0>*);
用-T(1)替换 T(-1),突然两个回归测试失败。(但是哪些呢?)
bool t01 = (!is_signed_integer<unsigned char>::value);
bool t02 = (!is_signed_integer<unsigned int>::value);
bool t03 = (!is_signed_integer<unsigned long long>::value);
bool t04 = (!is_signed_integer<unsigned long>::value);
bool t05 = (!is_signed_integer<unsigned short>::value);
bool t11 = (is_signed_integer<char>::value);
bool t12 = (is_signed_integer<int>::value);
bool t13 = (is_signed_integer<long long>::value);
bool t14 = (is_signed_integer<long>::value);
bool t15 = (is_signed_integer<short>::value);
失败的原因是“一元减”运算符将小的无符号整数提升为 int,所以-T(1)是 int,整个比较转移到 int 域,其中 0 > -1 为真。要查看这一点,请执行以下命令:
unsigned short u = 1;
identify(-u);
11.2.2.对数字常数的引用
通常,不要将静态常量直接传递给函数:
struct MyStruct
{
static const int value = 314;
}
int main()
{
double myarray[MyStruct::value];
std::fill_n(myarray, MyStruct::value, 3.14); // not recommended
}
如果 fill_n 通过 const 引用接受第二个参数,那么这段代码可能会导致链接失败。获取常数的地址需要在。cpp 文件(和其他静态成员一样)。在 TMP 中,很少出现这种情况。
作为一种廉价的解决方法,您可以构建一个临时整数并用常量初始化它:
// not guaranteed by the standard, but usually ok
std::fill_n(myarray, int(MyStruct::value), 3.14);
对于极端的可移植性,特别是对于枚举和 bool,您可以动态构建一个函数:
template <bool B> struct converter;
template <> struct converter<true>
{ static bool get() { return true; } };
template <> struct converter<false>
{ static bool get() { return false; } };
// instead of: DoSomethingIf(MyStruct::value);
DoSomethingIf(converter<MyStruct::value>::get());
11.3.常见解决方法
11.3.1 .调试 SFINAE
一个常见的“剪切和粘贴”错误是在函数中添加了一个无用的不可推导的模板参数。有时候,编译器会抱怨,但如果函数重载,SFINAE 原理 会默默将其排除在重载解析之外,一般会导致细微的错误:
template <typename X, size_t N>
static YES<[condition on X]> test(X*);
static NO test(...);
在这个片段中,不能推导出 N,因此总是选择第二个测试函数。
11.3.2.蹦床
编译器限制可能会影响蹦床。在经典 C++ 中,局部类有一些限制(它们不能绑定到模板参数)。它们可能会导致虚假的编译器和链接器错误:
template <typename T>
struct MyStruct
{
template <typename X>
void doSomething(const X& m)
{
struct local
{
static T* myFunc(const void* p)
{
// compilers may have problems here using template parameter X
}
};
// call local::myFunc(&m);
}
};
解决方法是将大部分模板代码移到本地类之外:
template <typename T>
struct MyStruct
{
template <typename X>
static T* MyFunc(const X& m)
{
// do the work here
}
template <typename X>
void DoSomething(const X& m)
{
struct local
{
static T* MyFunc(const void* p)
{
// put nothing here, just a cast
return MyStruct<T>::MyFunc(*static_cast<const X*>(p));
}
};
// ...
}
};
11.3.3.编译器错误
编译器错误很少见,但确实会发生,尤其是在模板元编程中。它们通常产生模糊的诊断结果。3
error C2365: 'function-parameter' : redefinition; previous definition was a 'template parameter'. see declaration of 'function-parameter'
在以下情况下,编译器会对模板感到困惑:
- 他们不能推断出表达式是一种类型。
- 它们没有正确地或以正确的顺序执行自动转换,因此会发出不正确的诊断。
- 一些语言关键字在静态上下文中可能无法正常工作。
这是最后一种说法的一个例子。如果表达式无效,sizeof 通常会抱怨。当您尝试取消引用 double 时,会发生以下情况:
int main()
{
sizeof(**static_cast<double*>(0));
}
error: illegal indirection
相同的测试可能无法正确触发 SFINAE。下面的代码用来用一个老版本的流行编译器打印“Hello”:4
template <size_t N>
struct dummy
{
};
template <typename X>
dummy<sizeof(**static_cast<X*>(0))>* test(X*)
{
printf("Hello");
return 0;
}
char test(...)
{
return 0;
}
int main()
{
double x;
test(&x);
}
下一个例子是由于隐式转换:
double a[1];
double b[1];
double (&c)[1] = true ? a : b;
error: 'initializing' : cannot convert from 'double *' to 'double (&)[1]'
A reference that is not to 'const' cannot be bound to a non-lvalue
因此,您可以看到编译器在三元运算符中将数组错误地转换为指针。然而,错误可能不会在模板函数中触发:
template <typename T>
void f()
{
T a;
T b;
T& c = true ? a : b;
}
f<double [1]>();
确保可移植性是一项重要的开发工作。可移植性的一个非正式定义是,“在多个平台上工作的代码,潜在地适应平台本身(带有预处理器指令,等等)”。符合标准的代码可以在任何地方工作,无需修改(假设编译器没有错误)。实际上,可移植性是符合标准的代码和解决特定编译器限制/错误的代码的结合。有些编译器有微妙的非标准行为;它们可能有扩展(例如,它们可能允许在堆栈上创建可变长度的数组),它们可能容忍较小的语法错误(例如 this- >或::template 的使用),甚至一些模糊性(例如,具有多个基的对象的静态转换)。然而,以标准一致性为目标是非常重要的,因为它保证了如果一段(元编程)代码工作了,它将继续工作,即使是在同一编译器的未来版本中。
如果看起来正确的代码无法编译,这可能有助于:
- 简化引入额外类型定义的复杂类型,反之亦然。
- 将功能提升为模板,反之亦然。
- 如果代码无法进一步更改,请测试不同的编译器。
见【http://en . cppreference . com/w/CPP/types/type _ info/name】。
2 另外,还存在交互式元调试器。元调试器在幕后使用他们自己的编译器,所以他们的输出可能与实际二进制文件中观察到的不同,但是当研究一个不编译的元函数时,他们是非常有价值的。这里可以找到一个:metashell.readthedocs.org/en/latest/
3 然而,它们是可能出错的好例子。
4 decltype 可能会遇到类似的问题。
十二、C++0x
“我注意到 C++0x 的每个特性都是由某个人在某个地方实现的。”
bjarne stroustup
在 2003 年的最终修订版中,我们习惯上称之为“经典 C++”,而不是 2011 年推出、随后在 2014 年完善的“现代 C++”(也非正式地称为 C++0x)。这一系列的改变是巨大的,但是新的规则总的来说是为了减轻 TMP 并使代码不那么冗长。此外,编译器提供了一系列新的标准类、容器、语言工具(如 std::bind)和特征,这些特征揭示了以前只有编译器知道的元信息。 1
最简单的例子就是元函数 std::has_trivial_destructor 。
仅仅通过语言来检测一个类型是否有一个简单的析构函数是不可能的。经典 C++ 中的最佳默认实现是“除非 T 是本机类型,否则返回 false”。 2
本章简要地触及了一个巨大主题的表面,所以不要认为这是一个完整的参考。为了更加清晰,一些描述被稍微简化了。
12.1.类型特征
编译器已经提供了一套完整的元函数:
#include <type_traits>
这将在名称空间 std 或 std::tr1 中带来一些元函数(取决于编译器和标准库)。 3
特别是,本书中描述的一些元函数出现在 C++0x 中,只是名称不同。下表列出了一些示例。4
|
本书
|
相当于 C++0x
| | --- | --- | | 静态值 | std::积分常数 | | 仅 _ 如果 | 标准::使能 _if | | 键入 if | 标准::有条件 | | 散列 _ 转换 | STD::is _ 可兑换 |
12.2. Decltype
与 sizeof 类似,decltype 解析为括号中给定的 C++ 表达式的类型(在运行时不计算它),您可以将它放在任何需要类型的地方:
int a;
double b;
decltype(a+b) x = 0; // x is double
decltype 可以对 SFINAE 产生积极的影响。以下元函数正确检测交换成员函数,测试表达式 x.swap(x ),其中 x 是对 x 的非常数引用。
由于 swap 通常返回 void,所以对通过测试的类型使用指向 decltype 的指针,对其余类型使用非指针类。然后,您像往常一样将它转换为是/否:
#define REF_TO_X (*static_cast<X*>(0))
struct dummy {};
template <typename T>
struct has_swap
{
template <typename X>
static decltype(REF_TO_X.swap(REF_TO_X))* test(X*);
static dummy test(...);
template <typename X>
static yes_type cast(X*);
static no_type cast(dummy);
static const bool value = sizeof( cast(test((T*)0)) )==sizeof(yes_type);
};
另外,C++11 头文件增加了一个新的功能,相当于宏 REF_TO_X。
在 SFINAE-expression 中,您可能会提到成员函数调用(前面的示例是“REF_TO_X.swap(REF_TO_X)的结果”),因此您需要 T 的一个实例。但是,您不能简单地调用构造函数,比如 T(),因为 T 可能没有公共的默认构造函数。一种解决方法是产生一个假引用,比如 REF_TO_X,因为表达式无论如何都不会被求值。但是在 C++11 中你可以只使用表达式 std::declval ()。这更安全,因为与宏相反,它只在未赋值的上下文中工作。
12.3.汽车
从 C++11 开始,关键字 auto 有了新的含义。它用于声明一个需要立即初始化的局部变量。初始化对象用于推断变量的实际类型,就像模板参数一样:
auto i = 0;
I 的实际类型与从调用 f(0)推导出的模板相同,其中 f 将是(伪代码):
template <typename auto>
void f(auto i);
auto 将总是解析为值类型。事实上,它的预期用途是存储来自一个函数的结果,而不明确提及它们的类型(想想 auto i = myMap.begin())。如果用户确实需要引用,可以显式限定 auto(作为任何模板参数):
const auto& i = cos(0.0);
auto 将解析为 double,因为调用 g(cos(0.0))时会发生这种情况,用
template <typename auto>
void g(const auto& i);
请记住,泛型模板参数不会与引用匹配:
int& get_ref();
template <typename T>
void f(T x);
f(get_ref()); // T = int, not reference-to-int
另一方面,decltype 返回表达式的精确静态类型,定义如下: 5
int i = 0;
decltype(get_ref()) j = i; // j is reference-to-int
decltype 有一些处理引用的规则:
- decltype(变量)或 decltype(类成员)导致与操作数相同的声明类型;如果 x 在当前范围内是 double,则 decltype(x)被推导为 double,而不是 double&。
- decltype(函数调用)是函数返回的结果类型。 6
- 如果前面的规则都不为真,并且表达式是 T 类型的左值,则结果为 T &;否则就是 t。
特别是一些“看起来很奇怪”的表达式,如 decltype(*&x)、decltype((x))或 decltype(true?x : x)将得到 double&因为没有一个操作数是普通变量,所以遵循第三条规则。
12.4.匿名函数
Lambda 表达式 (简称“lambdas”)提供了一种快速创建函数对象的简洁方法。它们不是一种新的语言特性,而是一种新的语法:
[](int i) { return i<7; }
[](double x, double y) { return x>y; }
每一行代表一个“仿函数”类型对象的实例(称为闭包),接受一个或多个参数并返回 decltype(返回语句)。所以你可以把这个对象传递给一个算法:
std::partition(begin, end, [](int i) { return i<7; });
std::sort(begin, end, [](double x, double y) { return x>y; });
这相当于更详细的:
struct LessThan7
{
bool operator()(int i) const
{
return i<7;
}
};
int main()
{
std::vector<int> v;
std::partition(v.begin(), v.end(), LessThan7());
}
明显的优点是更加清晰(执行分区的代码行变得独立)并且省略了不相关的信息(因为您不需要为函子及其参数找到一个有意义的名称)。
方括号[]被称为λ引入器,它们可以用来列出您想要“捕获”的局部变量,这意味着作为成员添加到函子中。在下面的例子中,闭包获得了 N 的副本(介绍者[ & N]将传递一个引用)。
int N = 7;
std::partition(v.begin(), v.end(), N { return i<N; });
同样,这个 lambda 相当于更详细的:
class LessThanN
{
private:
int N_;
public:
LessThanN(int N)
: N_(N)
{}
bool operator()(int i) const
{
return i<N;
}
};
还有一些语法细节。您可以在参数列表后显式指定返回类型。当您想要返回一个引用时,这确实很有用(默认情况下,返回类型是一个右值)。
[](int i) -> bool { ... }
可以使用 auto:
auto F = [](double x, double y) { return cos(x*y); }
最后,允许在成员函数中创建的 lambda 来捕捉这一点;lambda 函数调用操作符将能够访问原始上下文中可用的任何内容。实际上,lambda 主体的代码就像是直接写在声明它的地方一样。
class MyClass
{
private:
int myMember_;
void doIt() const { ... }
void doMore() { ... }
public:
int lambdizeMyself() const
{
auto L = [this]()
{
doIt(); // ok: doIt is in scope
doMore(); // error: doMore is non-const
return myMember_; // ok, private members can be read
};
return L();
}
};
下面的例子(来自 Stephan T. Lavavej)显示了 lambdas 可以与模板参数交互。这里λ用于执行未指定的一元谓词的逻辑否定。
template <typename T, typename Predicate>
void keep_if(std::vector<T>& v, Predicate pred)
{
auto notpred = &pred { return !pred(t); };
v.erase(remove_if(v.begin(), v.end(), notpred), v.end());
}
12.5.初始值设定项
如果一个函数有一个长返回类型,你可能不得不写两次——在函数签名中和在构建结果时。这种冗余很可能导致维护和重构问题。考虑 9.4.2 中的以下示例:
template <typename X>
console_assert<X, console_assert<T1, T2> > operator()(const X& x) const
{
return console_assert<X, console_assert<T1, T2> >(x, *this);
}
在经典的 TMP 中,这可以通过非显式的单参数构造函数来避免(可行时):
template <typename T1, typename T2>
class console_assert
{
public:
console_assert(int = 0) {}
};
template <typename X>
console_assert<X, console_assert<T1, T2> > operator()(const X& x) const
{
return 0; // much simpler, but we cannot pass parameters...
}
在 C++0x 中,一个新的语言特性叫做括号初始化列表允许你使用花括号构建一个对象,并且(在某些情况下)省略类型名:
std::pair<const char*, double> f()
{
return { "hello", 3.14 };
}
template <typename X>
console_assert<X, console_assert<T1, T2> > operator()(const X& x) const
{
return { x, *this };
}
编译器将根据重载决策规则,将初始值设定项列表中的项与所有构造函数的参数进行匹配,并选择最佳项。
12.6.模板类型定义
C++0x 用一个新的 using 语句扩展了传统的 typedef 语法:
typedef T MyType; // old syntax
using MyType = T; // new syntax
但是,新语法对模板也有效:
template <typename T>
using MyType = std::map<T, double>; // declares MyType<T>
MyType<string> m; // std::map<string, double>
12.7. 外部模板
12.7.1.链接模板
在经典 C++ 中,编译器需要看到函数/类模板的整个主体,才能生成模板实例。默认行为是只生成在翻译单元中实际使用的成员函数,所以粗略地说,每个。使用模板类的 cpp 文件将在相应的二进制对象中产生代码的副本。最后,链接器将收集所有的二进制对象并生成一个可执行文件,通常可以正确地识别和删除重复的对象。
在普通代码中,符号不能定义两次,但模板生成的代码被标记为“可重复”,最后一步中的链接器将删除 C++ 副本(如 vector:::size(),它生成了两次)和机器代码副本。它可能检测到所有的向量为每个 T 产生相同的汇编,所以最终的可执行文件将只包含每个成员函数的一个副本。
然而,这是因为 vector 头包含了所有相关的代码。让我们编写一个模板类,就像它是一个普通的类一样(记住,作为一个规则,这是不正确的)。
// xyz.h
template <typename T>
class XYZ
{
public:
int size() const;
};
// xyz.cpp
template <typename T>
int XYZ<T>::size() const
{
return 7;
};
现在,任何包含 xyz.h(以及针对 xyz.cpp 的链接)的翻译单元都能够正确编译任何代码,包括:
// main.cpp
#include <xyz.h>
int main()
{
XYZ<int> x;
return x.size();
}
但是,程序不会链接,因为在翻译单元 main.cpp 中,编译器看不到相关的模板体。另一方面,XYZ 可以在 xyz.cpp 内部充分利用*:*
// xyz.cpp
template <typename T>
int XYZ<T>::size() const
{
return 7;
};
int f()
{
XYZ<int> x; // Ok.
return x.size(); // Ok.
}
现在,作为副作用,二进制对象 xyz.obj 将包含所使用的相关成员函数的二进制代码(即构造函数 XYZ::XYZ()和 XYZ::size)。这意味着 main.cpp 现在可以正确链接了!
编译器将验证 main.cpp 的语法是否正确。由于它不能就地生成代码,它会将符号标记为“丢失”,但是链接器最终会找到并从 xyz.cpp 中借用它们。
不用说,这是可行的,因为两个文件都使用了 XYZ 和相同的成员函数。
该标准提供了一种在翻译单元中强制实例化模板及其所有成员函数的方法。这被称为显式实例化。
template class XYZ<int>;
可以使用名称空间和函数:
// assume that we included <vector>
template class std::vector<int>;
// assume that we included this template function:
// template <typename T>
// void f(T x)
template void f<int>(int x);
一种可能的用途是限制用户可以插入模板的类型集:
// xyz.cpp
template <typename T>
int XYZ<T>::size() const
{
return ...;
};
// these are the only types that the user will be able to plug in
// XYZ<T>. otherwise the program won't link.
template class XYZ<int>;
template class XYZ<double>;
template class XYZ<char>;
现在,这个翻译单元将包含 XYZ 所有成员函数的二进制代码,因此在组装最终的可执行文件时,它们可以被正确地“导出”到其他单元。
12.7.2. 外部模板
在 C++0x 中(作为许多经典 C++ 编译器的扩展),有可能阻止编译器自动实例化一个模板,并强制执行上一节中描述的行为。
extern template class XYZ<int>;
这迫使模板类像普通类一样链接(特别是,内联仍然是可能的),并且可以节省编译时间。
根据 C++ 标准,这种语法阻止隐式实例化,但是不阻止显式实例化。因此原则上可以将单个 extern 模板声明放在。hpp 文件(在模板代码之后),以及. cpp 文件中的单个显式实例化。 7
////////////////////////////
// special_string.hpp
template <typename T>
class special_string
{
public:
int size() const { ... }
};
extern template special_string<char>;
extern template special_string<wchar_t>;
////////////////////////////
// special_string.cpp
#include "special_string.hpp"
template special_string<char>;
template special_string<wchar_t>;
12.9.可变模板
从 C++11 开始,模板参数列表可以有可变的长度:
template <typename... T>
struct typearray
{};
template <size_t... N>
struct list_of_int
{};
typearray<int> t1; // Ok.
typearray<int, double, float> t3; // Also ok.
typearray<> t0; // An empty list also works.
省略号(。。。)到 T 左边的声明 T 可以匹配一个(可能是空的)参数列表。t 确实叫做模板参数包*。另一方面,包含参数包名称的表达式右边*的省略号扩展了它(简单地说,它克隆了包中每种类型的表达式):**
template <typename... T>
void doSomething(T... args) // conceptually equivalent to:
// T1 arg1, T2 arg2, ... , Tn argn
{
typearray<T> e; // error: T is unexpanded
typearray<T...> e; // ok, gives: <T1, ..., Tn>
list_of_int<sizeof(T)...> l; // ok, gives: <sizeof(T1), ..., sizeof(Tn)>
}
您可以使用模式匹配来“迭代”一个参数包:
void doSomething() // will match 0 arguments
{
}
template <typename HEAD, typename... TAIL>
void doSomething(HEAD h, TAIL... tail) // will match 1 or more arguments
{
std::cout << h << std::endl;
doSomething(tail...);
}
作为练习,看一下这个元函数计数,它计算 T 类型在包 A 中出现的次数:
template <typename T, typename... A>
struct count;
template <typename T, typename... A>
struct count<T, T, A...>
{
static const int value = 1 + count<T, A...>::value;
};
template <typename T, typename T2, typename... A>
struct count<T, T2, A...> : count<T, A...>
{};
template <typename T>
struct count<T> : std::integral_constant<int, 0>
{};
省略号可以同时触发多个扩展。例如,假设您想要检查包中的类型是否重复两次:
template <typename T, typename... A>
int assert()
{
static_assert(count<T, A...>::value <= 1, "error");
return 0;
}
template <typename... N>
void expand_all(N...)
{
}
template <typename... A>
void no_duplicates(A... a)
{
expand_all(assert<A, A...>()...); // double expansion
}
这种双重扩展将调用:
expand_all(assert<A1, (all A)>(), assert<A2, (all A)>(), ...)
expand_all 获取任意数量的任意类型的参数,并完全忽略它们。这对于触发参数包的扩展是必要的。实际上,所有 assert <...>函数要么编译失败,要么返回 0,所以 no_duplicates 很容易被内联,几乎不产生任何代码。
1 由于形势发展迅速,请参考在线文档。找到一个同时完整和最新的对照表并不容易,但在撰写本文时,好的参考资料是【wiki.apache.org/stdcxx/C++0… . com/c11-compiler-support-shootout-visual-studio-gcc-clang-Intel/`](cpprocks.com/c11-compile…
然而,一般来说,元函数返回一个“次优”值是可以接受的。如果已知一个类的析构函数很简单,那么可以优化代码。像“没有析构函数是微不足道的”这样激烈的假设可能会使程序变慢,但它不应该出错。
3 它们在可免费下载的“C++ 库扩展技术报告草案”(www . open-STD . org/JT C1/sc22/wg21/docs/papers/2005/n 1836 . pdf)中有所描述。
兼容 C++11 的编译器附带的元函数列表可以在这里找到:【en.cppreference.com/w/cpp/heade…
5 关于 auto 和 decltype 区别的详细解释,参见【17】。
6
当两个指令都存在时,旧的编译器不需要考虑这种行为,所以可能需要使用一些预处理器。*