道高一尺 or 魔高一丈:浅谈交互库编写
有些时候,为了实现特殊的需求,我们可能需要编写交互库,要求选手链接。
常见的情景有:
- 强制在线:一些题目不强制在线可能会被“乱搞”通过。例如可持久化数据结构有众所周知的离线做法。
- 加速输入输出:一些题目为了要求严格线性可能需要 $10^8$ 以上的输入量,如此大的数据必须在内存生成并交换。
- 限制操作:一些思维题不允许选手直接读取数据,而是要求选手做特定询问获取详细内容;或者可能限制操作次数。
- 人机对抗:另一些思维题要求选手找到最优策略,那么可以要求选手通过接口与交互库对抗。
既然交互库要链接选手的程序,就必须做好防范措施,避免选手使用不当操作 $\textcolor{green}{\text{AC}}$。
本人才疏学浅,谈论的内容只是全局的一小部分,欢迎补充。
如果不要求选手进行输出,很多交互库可能只是简单输出 Yes
或 No
来告诉 Judger 结果是否正确。
一个基本的素养是将除了 main
函数和接口函数以外的函数或变量使用 static
修饰,并且不使用 #define
或 using
。防止出现奇怪问题或被选手猜中变量名。
这里有一个显然的漏洞。众所周知,函数 exit
可以让程序退出,不必返回主函数,选手自然可以输出 Yes
后直接退出。
int solve(...) {
...
cout << "Yes" << endl;
exit(0);
}
最简单的方式是不告诉选手交互库评测时行为。
不过,在部分题目中,这可能是无法避免的。
一个简单的做法是利用析构函数,只要定义一个全局变量,程序退出时自然会进行析构。
static struct E {
int code = -1;
~E() { cout << code << endl; }
} e;
另一个高级的方法是使用 atexit
注册函数,被注册的函数会在程序正常退出时被逆序调用。
atexit
定义在 <cstdlib>
。
...
static int code;
int main() {
...
auto p = []{ cout << code << endl; };
atexit(p);
...
}
要注意的是以上两种方法只能在函数正常退出时输出;不过程序非正常退出时一定不会返回 0
,会出现 $\textcolor{purple}{\text{RE}}$。
不过最近看到一个有趣的函数:
[[noreturn]] void quick_exit( int exit_code ) noexcept;
这个函数也定义在 <cstdlib>
。顾名思义,这个函数可以使程序快速退出,不完全清理资源。
#include <cstdlib>
#include <iostream>
using namespace std;
static struct E { ~E() { cout << "clean" << endl; } } e;
int main() { quick_exit(0); }
尝试运行以上程序,你会发现不会输出任何东西。
不过如果你知道 quick_exit
,一定会看到 at_quick_exit
,后者也定义在 <cstdlib>
。
at_quick_exit
可以注册快速退出时执行的函数,不会在正常退出时调用。
这段程序可能更加保险。
...
static int code;
int main() {
...
auto p = []{ cout << code << endl; };
auto q = []{ cout << "at_quick_exit" << endl; };
atexit(p);
at_quick_exit(q);
...
}
注意快速退出时不一定刷新缓冲区,需要手动刷新。
至此,在输出上直接做手脚可能比较困难,但选手程序和交互库毕竟被编译到同一程序,高手可以选择直接操作内存。
参见 部分交互题的通用 hack 方法,By mcfxmcfx。
有灵感在记录罢。