【发布时间】:2015-09-25 13:59:48
【问题描述】:
见BlendingTable::create 和BlendingTable::print。两者都有相同形式的尾递归,但是create 将被优化为循环,print 不会并导致堆栈溢出。
下去查看修复,这是我从一个 gcc 开发人员那里得到的关于这个问题的错误报告的提示。
#include <cstdlib>
#include <iostream>
#include <memory>
#include <array>
#include <limits>
class System {
public:
template<typename T, typename... Ts>
static void print(const T& t, const Ts&... ts) {
std::cout << t << std::flush;
print(ts...);
}
static void print() {}
template<typename... Ts>
static void printLine(const Ts&... ts) {
print(ts..., '\n');
}
};
template<typename T, int dimension = 1>
class Array {
private:
std::unique_ptr<T[]> pointer;
std::array<int, dimension> sizes;
int realSize;
public:
Array() {}
template<typename... Ns>
Array(Ns... ns):
realSize(1) {
checkArguments(ns...);
create(1, ns...);
}
private:
template<typename... Ns>
static void checkArguments(Ns...) {
static_assert(sizeof...(Ns) == dimension, "dimension mismatch");
}
template<typename... Ns>
void create(int d, int n, Ns... ns) {
realSize *= n;
sizes[d - 1] = n;
create(d + 1, ns...);
}
void create(int) {
pointer = std::unique_ptr<T[]>(new T[realSize]);
}
int computeSubSize(int d) const {
if (d == dimension) {
return 1;
}
return sizes[d] * computeSubSize(d + 1);
}
template<typename... Ns>
int getIndex(int d, int n, Ns... ns) const {
return n * computeSubSize(d) + getIndex(d + 1, ns...);
}
int getIndex(int) const {
return 0;
}
public:
template<typename... Ns>
T& operator()(Ns... ns) const {
checkArguments(ns...);
return pointer[getIndex(1, ns...)];
}
int getSize(int d = 1) const {
return sizes[d - 1];
}
};
class BlendingTable : public Array<unsigned char, 3> {
private:
enum {
SIZE = 0x100,
FF = SIZE - 1,
};
public:
BlendingTable():
Array<unsigned char, 3>(SIZE, SIZE, SIZE) {
static_assert(std::numeric_limits<unsigned char>::max() == FF, "unsupported byte format");
create(FF, FF, FF);
}
private:
void create(int dst, int src, int a) {
(*this)(dst, src, a) = (src * a + dst * (FF - a)) / FF;
if (a > 0) {
create(dst, src, a - 1);
} else if (src > 0) {
create(dst, src - 1, FF);
} else if (dst > 0) {
create(dst - 1, FF, FF);
} else {
return;
}
}
void print(int dst, int src, int a) const {
System::print(static_cast<int>((*this)(FF - dst, FF - src, FF - a)), ' ');
if (a > 0) {
print(dst, src, a - 1);
} else if (src > 0) {
print(dst, src - 1, FF);
} else if (dst > 0) {
print(dst - 1, FF, FF);
} else {
System::printLine();
return;
}
}
public:
void print() const {
print(FF, FF, FF);
}
};
int main() {
BlendingTable().print();
return EXIT_SUCCESS;
}
将System的类定义从
class System {
public:
template<typename T, typename... Ts>
static void print(const T& t, const Ts&... ts) {
std::cout << t << std::flush;
print(ts...);
}
static void print() {}
template<typename... Ts>
static void printLine(const Ts&... ts) {
print(ts..., '\n');
}
};
到
class System {
public:
template<typename T, typename... Ts>
static void print(T t, Ts... ts) {
std::cout << t << std::flush;
print(ts...);
}
static void print() {}
template<typename... Ts>
static void printLine(Ts... ts) {
print(ts..., '\n');
}
};
神奇地允许 gcc 消除尾调用。
为什么“是否通过引用传递函数参数”会对 gcc 的行为产生如此大的影响?在这种情况下,它们在语义上看起来都一样。
【问题讨论】:
-
我真的很好奇 perfect forwarding 是否会出现同样的问题。
-
一般来说,通过引用传递会使编译器的程序分析更加困难,因此更难确定哪些转换是合法的。无论这是这里的实际问题还是有其他原因(比如标准中的一个怪癖,这使得这种优化是非法的)我不能说
-
一个假设是强制转换创建了一个临时文件,其引用被传递给
print函数,现在编译器可能觉得它需要挂在临时文件上直到print函数返回。
标签: c++ gcc recursion functional-programming tail-call-optimization