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)被人发现。。。)

外部链接

结论

ps: 和TMP类似,TMP并不是模板的初衷,但是被发现后已经被广泛的应用(包括C++11起的标准库),编译器进行计算虽然使得编译时间变长,但却可以避免运行期的开销。不过,代码的可读性就几乎没有了,这也使得编码的门槛更高了。 TMP的出现迫使C++标准委员会为C++添加了新的特性(比如C++11起的type_traits),那么SMP呢?
pps: 感谢,C++的编译器实现者们!



转载请注明:Serenity » Stateful Metaprogramming

上一篇

下一篇