Stateful Metaprogramming
Disclaimer: The technique described in this post is primarily meant as “just another clever hack, diving into the dark corners of C++”.
导语
首先,请添加代码(禁止使用宏,使用宏没有任何意义),使得下面的代码能通过编译
int main () {
constexpr int a = f ();
constexpr int b = f ();
static_assert (a != b, "fail");
}
你或许和我一样会觉得不可能,想要在编译期保存状态还要改变它,然而它又是不可变的。。。然而,是有办法实现的,(constexper自C++11起,个人认为这是C++11、C++14(以及C++17?)标准的一个bug)而且符合现有的C++标准。
准备
ADL (Argument Dependent Lookup)
虽然这个特性已经在实践中使用了无数次,但是从来没有引起过使用者的注意,大都是习以为常了。
#include <iostream>
namespace nm
{
struct Foo
{
std::string name() { return "Foo"; }
};
void foo(Foo f) { std::cout << f.name() << std::endl; }
}
int main()
{
foo(nm::Foo{});
}
以上代码,初看会觉得编译不过,因为foo
并没有在global namespace。但是,你可曾想过std::cout
打印std::string
的重载是定义在什么地方的?
friend
同样,又是一个使用了无数次的东西。
示例1
#include <iostream>
class A;
void update(A&);
class A
{
friend void update(A&);
std::string name_;
public:
void print()
{
std::cout << name_ << std::endl;
}
};
void update(A& a)
{
a.name_ = "class A";
}
int main()
{
A a;
update(a);
a.print(); // print 'class A'
}
示例2
class A
{
public:
A(int) {}
void print()
{
std::cout << name_ << std::endl;
}
private:
friend void update(A& a)
{
a.name_ = "class A";
}
std::string name_;
};
int main()
{
A a = 0;
update(a);
update(0); // error: ‘update’ was not declared in this scope
a.print(); // print 'class A'
}
你可能会说,看起来没什么特别的啊,这两个片段可以说是一样的。
但是,你把示例和前面ADL的代码比较一下呢?
再深入一点 > 7.3.1.2⁄3 Namespace member definitions [namespace.memdef]p3
Every name first declared in a namespace is a member of that namespace. If a friend declaration in a non-local class first declares a class, function, class template or function template the friend is a member of the innermost enclosing namespace. The friend declaration does not by itself make the name visible to unqualified lookup (3.4.1) or qualified lookup (3.4.3).
friend 相关的模板实例化语义
If a class template specialization contains friend-declarations, the names of its friends are treated as if an explicit specialization had been declared at the point of instantiation.
解决
不说了,直接上代码(代码抄自这里)
ps: The implementtation
里的代码g++以及clang++均编译不过,以下是Workground for clang
namespace detail {
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 = detail::A>
struct dependent_writer : detail::writer<Tag> { };
template<
class Tag = detail::A,
bool B = is_flag_usable<Tag> (0),
int = sizeof (dependent_writer<B>)
>
constexpr int f () {
return B;
}
int main () {
// 1st call, the adl_flag in struct A has not been specialized
// to avoid ill-formed function call(is_flag_usable), C++ standard
// allow SFINAE, so the less matched function `is_flag_usable(...)`
// is the chosen one. Thus, B is false, then compiler starting
// instantiate struct dependent_writer, and this cause ADL for adl_flag
// and found its definition in struct writer.
/// we can also do this
/// ```c++
/// template<
/// class Tag = detail::A,
/// bool B = is_flag_usable<Tag> (0),
/// class = decltype(dependent_writer<B>{})
/// >
/// ```
/// or just put `dependent_writer<B>` into the function body
/// ```c++
/// template<
/// class Tag = detail::A,
/// bool B = is_flag_usable<Tag> (0)
/// >
/// constexpr int f () {
/// dependent_writer<B>{};
/// return B;
/// }
//.```
constexpr int a = f ();
// 2nd call, all functions and classes were specialized. Thus, the
// call is_flag_usable<Tag>(0) will perfectly match `is_flag_usable(int)`
// so, this time B is true.
constexpr int b = f ();
static_assert (a != b, "fail");
}
代码解释,见注释。
再解释下为啥要用一个dependent_writer
,这是因为需要这个类的特化(一个dependent_write<false>
一个dependent_writer<true>
)。但是继承自writer
却不是必须的,也可以把writer
直接丢到类内部,保证有一次实例化就行了
template<bool B, class Tag = detail::A>
struct dependent_writer {
static constexpr detail::writer<Tag> dummy = detail::writer<Tag>{};
};
或者,你真的不想要dependent_writer
,那么你也可以这样
template<class Tag = detail::A, bool B = is_flag_usable<Tag>(0)>
constexpr int f () {
constexpr auto dummy = detail::writer<Tag>{};
(void)dummy; // shut up compiler
return B;
}
Q: stateful,所以,状态是怎么产生的呢?
A: 如你所见,friend injection + ADL + SFINAE + constexpr + 模板实例化(以及特化)的顺序共同导致了状态的产生。(不怀好意的说,就是由于C++标准的模板部分一直存在各种UB(undefined behavior),加上后来补漏洞新添加的constexpr导致了新的feature(bug,这个stateful并不是UB)被人发现。。。)
外部链接
结论
- C++的模板是用来搞 TMP 的吗? (TMP发现有些年头了)
- C++的ADL、TMP是用来搞 SMP 的吗?(SMP比较新, constexpr都可变了还叫constexpr吗??)
- C++ METAPROGRAMMING: EVOLUTIONAND
FUTURE DIRECTIONS
LOUIS DIONNE, MEETING C++ 2016
ps: 和TMP类似,TMP并不是模板的初衷,但是被发现后已经被广泛的应用(包括C++11起的标准库),编译器进行计算虽然使得编译时间变长,但却可以避免运行期的开销。不过,代码的可读性就几乎没有了,这也使得编码的门槛更高了。 TMP的出现迫使C++标准委员会为C++添加了新的特性(比如C++11起的type_traits),那么SMP呢?
pps: 感谢,C++的编译器实现者们!