Amethyst Studio
977 words
5 minutes
C++黑魔法 - 编译期变量
2023-03-28

注意:本篇blog中的代码涉及到Undefined Behaviour,不建议在生产环境中使用。

Question#

我们想实现一个非常特殊的constexpr函数,使得:

constexpr int f();

int main() {
  constexpr int a = f();
  constexpr int b = f();

  static_assert(a != b, "failed!");
  return 0;
}

注意在这一篇blog当中,我们只要求两次调用结果不一致,不需要更多次的调用结果不一致。

注意这个要求实际上非常不容易达成,因为这里的f本身就是constexpr,理论上来说,这种constexpr函数的返回值应该是确定的才是。这道题目要想解出,需要相当熟悉C++的编译器才行。以下是答案:


Solution#

struct A {
  constexpr A() {}
  friend constexpr int adl_flag(A);
};

template <class Tag> struct writer {
  friend constexpr int adl_flag(Tag) { return 0; }
};

template <class Tag, int = adl_flag(Tag{})>
constexpr bool is_flag_usable(int) {
  return true;
}

template <class Tag>
constexpr bool is_flag_usable(...) { return false; }

template <bool B, class Tag = A> struct dependent_writer : writer<Tag> {};

template <class Tag = A, bool B = is_flag_usable<Tag>(0),
          int = sizeof(dependent_writer<B>)>
constexpr int f() {
  return B;
}

int main() {
  constexpr int a = f();
  constexpr int b = f();
  static_assert(a != b, "failed!");
  return 0;
}

以上的代码可以使用g++通过编译(但clang++无法通过)。


Explanation#

上面的答案并不好理解,一行一行来看。

首先,我们声明一个类,这个类当中有一个友元函数:

struct A {
  constexpr A() {}
  friend constexpr int adl_flag(A); // Notice we don't implement this function
};

注意,这里我们特意没有去实现这个函数,这是刻意为之,后面会解释。

然后,我们先跳过中间的Writer类,看一下第一个is_flag_usable

template <class Tag, int = adl_flag(Tag{})>
constexpr bool is_flag_usable(int) {
  return true;
}

这个时候,这个is_flag_usable无论如何都是不能直接使用的,因为并不存在一个adl_flag(Tag)函数,包括adl_flag(A)。也就是说,不管怎么使用,这个is_flag_usable都会出现代入失败。

接着看下面的一行:

template <class Tag>
constexpr bool is_flag_usable(...) { return false; }

这个的意思就是,因为前面返回trueis_flag_usable无论如何都会代入失败,而代入失败的函数都会经过这个函数。那么到这里,如果我们直接使用is_flag_useable,不管参数是什么,都将返回false

is_flag_useable解释完之后,接着,我们来看writer类:

template <class Tag> struct writer {
  friend constexpr int adl_flag(Tag) { return 0; }
};

而对于writer类,它是有一个adl_flag的实现的。

那么,仔细想想,在前面的**A类的时候,** adl_flag是没有实现的。但是到了writer类,假如这里的Tag = A ,那么我们不就有了一个adl_flag(A)了吗?

所以,对于f函数:

template <class Tag = A, bool B = is_flag_usable<Tag>(0),
          int = sizeof(dependent_writer<B>)>
constexpr int f() {
  return B;
}

那么,在第一次调用f的时候,这里的is_flag_usable因为adl_flag(A)代入失败,因此恒定地返回了一个false值。但是,这个函数的第三个木板参数int = sizeof(depent_writer<B>),这个模板参数本身的值没有意义,它的意义在于实例化了一个dependent_writer的对象:

template <bool B, class Tag = A> struct dependent_writer : writer<Tag> {};

这个dependent_writer,是继承了writer,而这个类的一旦被实例化,adl_flag(A)就会被启动,从而在第二次调用f()函数的时候f的模板is_flag_usable会返回true。因而前后两次f函数的调用会不一致。

但是不仅于此,再仔细观察f的第三个模板参数:

int = sizeof(dependent_writer<B>)

会发现,这里dependent_writer<B>,使用这个B非常关键,因为这样以来就是强制要求编译器必须在实例化depent_writer之前,先取得B的确切的值。如果这里的B换成其它的参数,同样也会失效。

所以,通过这个例子其实可以猜到编译器的一些内幕:

  1. g++不会记录constexpr函数的值,而是遇到一次计算一次。而clang++可能会记录constexpr函数的值。

More Detail#

C++黑魔法 - 编译期变量
https://ziyue.cafe/posts/black-magick-compile-time-mutable-in-cpp/
Author
Kaida Amethyst
Published at
2023-03-28