BOOST_PP_SEQ_FOLD_LEFT是如何工作的?

11

我需要编写一个处理任意长度的类似 (A)(B)(C) 的列表的宏。如果我可以使用 Boost 依赖,我会直接使用其中一个 BOOST_PP_SEQ_ 宏族。不幸的是,我不能使用它,因此我必须设法弄清它的工作原理。这些东西并不明显。

这里有没有人可以为我编写一个简单的、独立的实现,例如BOOST_PP_SEQ_FOLD_LEFT,让我参考?特别是,我想转换:

template_(class A, class B, class C)(
    requires IsFoo<A> && IsBar<B>)(
    requires IsBaz<C>)
void frobozzle(A, B, C);

需要重新书写为:

template<class A, class B, class C,
    int dummy = 0,
    std::enable_if_t<dummy == 0 && (IsFoo<A> && IsBar<B>), int> = 0,
    std::enable_if_t<dummy == 0 && (IsBaz<C>), int> = 0>
void frobozzle(A, B, C);

可以有任意数量的requires子句,它们应该分别获得自己的enable_if_t。我已经用一个requires子句使其工作了,但是在此过程中,我耗尽了我的C预处理器技能。

可以假设使用符合标准的预处理器,因为我不需要MSVC支持。


使用 (...)__VA_ARGS__ 对你有用吗?我不确定你在问什么。 - user8782808
我更新了问题描述以提供我的动机用例,但我只需要知道如何对列表中的每个元素执行某些操作,例如(A)(B)(C)。其余部分我可以自己完成。 - Eric Niebler
你对语法的要求有多严格?它需要是(A)(B)(C),还是((A)(B)(C))也可以? - Mara Bos
3个回答

13

如果在语法中添加额外的一组括号,则可以使用相对较少的宏而不限制“必需”子句的数量:

template_((class A, class B, class C)
    (requires IsFoo<A> && IsBar<B>)
    (requires IsBaz<C>)
)
void frobozzle(A, B, C);

宏:

#define template_(...) template_impl_ADD_END(template_impl_LIST __VA_ARGS__) >

#define template_impl_ADD_END(...) template_impl_ADD_END2(__VA_ARGS__)
#define template_impl_ADD_END2(...) __VA_ARGS__ ## _END

#define template_impl_LIST(...) template<__VA_ARGS__, int dummy = 0 template_impl_LIST_1

#define template_impl_LIST_1(...) , std::enable_if_t<dummy == 0 && template_impl_REQUIRES(__VA_ARGS__), int> = 0 template_impl_LIST_2
#define template_impl_LIST_2(...) , std::enable_if_t<dummy == 0 && template_impl_REQUIRES(__VA_ARGS__), int> = 0 template_impl_LIST_1

#define template_impl_REQUIRES(...) (template_impl_REQUIRES_ ## __VA_ARGS__)
#define template_impl_REQUIRES_requires

#define template_impl_LIST_END
#define template_impl_LIST_1_END
#define template_impl_LIST_2_END

使用这些宏,上面的示例会扩展为:
template <class A, class B, class C,
          int dummy = 0,
          std::enable_if_t<dummy == 0 && (IsFoo<A> && IsBar<B>), int> = 0,
          std::enable_if_t<dummy == 0 && (IsBaz<C>), int> = 0>

void frobozzle(A, B, C);

解释

考虑以下宏定义:

#define a(x) [x] b
#define b(x) [x] a

使用这些,这个:

a (1) (2) (3) (4)

会引起以下“连锁反应”的扩张:
a (1) (2) (3) (4)
[1] b (2) (3) (4)
[1] [2] a (3) (4)
[1] [2] [3] b (4)
[1] [2] [3] [4] a

递归在预处理器中不被允许,但这种链式反应不是递归,因为宏的调用仅发生在前一个宏扩展后,而不是在其期间,因为(不是扩展的一部分。(尽管请参见https://wg21.link/cwg268)。不幸的是,虽然这将很好地循环遍历一系列(A)(B)(C),但它会在末尾留下一个额外的标记: 两个使用的宏之一的名称。我用来摆脱它的技巧是用另一个宏调用包装整个列表,它将在完全扩展后附加(使用连接运算符##)_END,所以它将变成:
[1] [2] [3] [4] a_END

然后我们可以通过定义以下内容来简单地摆脱最后一个标记:
#define a_END
#define b_END

如果我们无法包含整个列表,就没有办法知道何时到达最后一个元素。唯一发生的事情是最后一个 ab 没有后面跟随的 ( ,这意味着它不会被展开,因为 ab 是函数式宏。(我们不能只定义 ab 为空,因为 ab 已经是宏,尽管它们没有 ( 时不会被展开。)

为什么需要两个宏?

当我们尝试像上面那样使用一个宏来引起连锁反应时,会出现问题,因此需要使用两个宏,如下所示:

#define a(x) [x] a

它不会起作用:

a (1) (2) (3) (4)
[1] a (2) (3) (4) // Doesn't expand further

这是由于“(无限)递归保护”机制的工作方式:在宏扩展期间,如果产生了与正在扩展的宏名称相同的令牌,则该令牌被标记为“不可扩展”,这意味着它永远不会再次扩展。详见http://eel.is/c++draft/cpp.rescan#2 这意味着扩展的a被标记为“不可扩展”,我们的连锁反应在第一步后立即停止。我们通过使用两个宏来解决此规则:a(..)将不会产生任何带有其自己名称的标记,而只会使用另一个宏b的名称。 a的扩展就在此结束,在 b 被扩展之前,因为在 a 的扩展中还没有(。扩展完成后,我们不再“在 a 内”,令牌被重新检查,找到b(..)的正确调用。它将再次生成具有名称a的标记,但由于我们不再处于第一个a 的扩展中,因此不会将其标记为“不可扩展”,连锁反应继续进行。

2
非常好! :-) - Eric Niebler
非常好,我也尝试过类似的方法,但从未像我实现的那样将#define template_impl_ADD_END2(...) __VA_ARGS__ ## _END工作起来,因为我使用了BOOST_PP_CAT(requiresclauses , _end),而由于requiresclauses正在展开逗号,所以它没有起作用。你解决这个问题的方式相当不错。 - daminetreg

2

好的,这是我快速制作的一个简单粗糙的东西,我认为你可以使用:

#include <iostream>

#define LIST (1)(2)(3)(4)

#define EAT2(list)
#define EAT(list) EAT2 list
#define KEEP(x) x EAT2(
#define STRINGIFY2(x) #x
#define STRINGIFY(x) STRINGIFY2(x)

#define HEAD(list) KEEP list )
#define TAIL(list) EAT(list)
int main()
{
    std::cout << STRINGIFY(HEAD(LIST)) << std::endl;
    std::cout << STRINGIFY(TAIL(LIST)) << std::endl;
}

基本上,您需要在调用宏时进行一些技巧处理。
以以下示例为例:
HEAD((1)(2))

扩展为

KEEP (1)(2) )

该术语扩展为

1 EAT2 ((2))

扩展为

1

这不是一个完整的答案,但我认为可以作为你想要做的事情的起点。

编辑

我现在已经弄清楚了BOOST.PP如何进行迭代,这并不美观,你基本上需要手动编写迭代代码,直到达到某个最大尺寸。

#define CONCAT2(x, y) x##y
#define CONCAT(x, y) CONCAT2(x, y)

#define SEQ_SIZE(seq) CONCAT(SEQ_SIZE_, SEQ_SIZE_0 seq)

# define SEQ_SIZE_0(_) SEQ_SIZE_1
# define SEQ_SIZE_1(_) SEQ_SIZE_2
# define SEQ_SIZE_2(_) SEQ_SIZE_3
# define SEQ_SIZE_3(_) SEQ_SIZE_4
# define SEQ_SIZE_4(_) SEQ_SIZE_5
# define SEQ_SIZE_5(_) SEQ_SIZE_6
# define SEQ_SIZE_6(_) SEQ_SIZE_7
# define SEQ_SIZE_7(_) SEQ_SIZE_8
# define SEQ_SIZE_8(_) SEQ_SIZE_9
# define SEQ_SIZE_9(_) SEQ_SIZE_10
# define SEQ_SIZE_10(_) SEQ_SIZE_11
# define SEQ_SIZE_SEQ_SIZE_0 0
# define SEQ_SIZE_SEQ_SIZE_1 1
# define SEQ_SIZE_SEQ_SIZE_2 2
# define SEQ_SIZE_SEQ_SIZE_3 3
# define SEQ_SIZE_SEQ_SIZE_4 4
# define SEQ_SIZE_SEQ_SIZE_5 5
# define SEQ_SIZE_SEQ_SIZE_6 6
# define SEQ_SIZE_SEQ_SIZE_7 7
# define SEQ_SIZE_SEQ_SIZE_8 8
# define SEQ_SIZE_SEQ_SIZE_9 9
# define SEQ_SIZE_SEQ_SIZE_10 10

#define MAKE_VAR(elem)                         \
    float CONCAT(var_, elem) = 0;

#define MAKE_LIST_0(op, list)
#define MAKE_LIST_1(op, list)  op (HEAD(list)) MAKE_LIST_0(op, TAIL(list))
#define MAKE_LIST_2(op, list)  op (HEAD(list)) MAKE_LIST_1(op, TAIL(list))
#define MAKE_LIST_3(op, list)  op (HEAD(list)) MAKE_LIST_2(op, TAIL(list))
#define MAKE_LIST_4(op, list)  op (HEAD(list)) MAKE_LIST_3(op, TAIL(list))
#define MAKE_LIST_5(op, list)  op (HEAD(list)) MAKE_LIST_4(op, TAIL(list))
#define MAKE_LIST_6(op, list)  op (HEAD(list)) MAKE_LIST_5(op, TAIL(list))
#define MAKE_LIST_7(op, list)  op (HEAD(list)) MAKE_LIST_6(op, TAIL(list))
#define MAKE_LIST_8(op, list)  op (HEAD(list)) MAKE_LIST_7(op, TAIL(list))
#define MAKE_LIST_9(op, list)  op (HEAD(list)) MAKE_LIST_8(op, TAIL(list))
#define MAKE_LIST_10(op, list)  op (HEAD(list)) MAKE_LIST_9(op, TAIL(list))

#define MAKE_LIST(op, list) CONCAT(MAKE_LIST_, SEQ_SIZE(list)) (op, list)

int main()
{
    MAKE_LIST(MAKE_VAR, LIST)
}

运行预处理器会得到以下结果:
int main()
{
    float var_1 = 0; float var_2 = 0; float var_3 = 0; float var_4 = 0; float var_5 = 0;
}

按照您的要求,我相信这可以进一步简化,但是我希望这能有所帮助。


文档声称像BOOST_PP_SEQ_FOLD_LEFT这样的东西可以处理的元素数量没有限制(尽管像SIZEELEM这样的东西可能会有限制)。我仍然想知道如何实现这一点。 - Eric Niebler
值得注意的是,虽然序列的长度没有上限,但是像BOOST_PP_SEQ_ELEM这样需要数字参数的操作只能使用256以下的值。 - Eric Niebler
1
@EricNiebler 很抱歉告诉你,我刚刚尝试了一个包含275个元素的序列示例,BOOST_PP_SEQ_FOLD_LEFT失败了。当我将该序列缩减到约160个元素时,它可以正常工作,因此我认为BOOST_PP_FOLD_LEFT存在大小限制。 - SirGuy
1
就我个人而言,我认为Chaos可以处理更长的序列。这里似乎有一个副本:https://github.com/ldionne/chaos-pp - Daniel James
在宇宙热寂之前,我没有希望理解混沌是如何工作的。 :-( - Eric Niebler
显示剩余5条评论

0

这是我的一点建议:

我在记忆中遇到的问题是,在 Boost.Preprocessor 中使用预处理器元编程技术时,当折叠一个序列时,无法拥有任意长的元素列表。

您需要尽可能多的宏来进行最大迭代,因此可以是任意的,但有一个最大值。

然而,我想知道是否可以扩展逗号,因为通常这是基于与下一个停止条件宏或下一个迭代宏连接起来。我不知道如何从连接的宏中扩展逗号,因为连接将不再起作用。

对于这种情况,如果您可以稍微更改 API,我会这样做:

#define EXPAND(...) __VA_ARGS__

#define template_(X, Y)   \
  template<EXPAND X       \
    , int dummy = 0       \
  Y                       \
>

#define requires(...) \
  COMMA() std::enable_if_t< dummy == 0 && (__VA_ARGS__) > = 0  

#define COMMA() ,

所以,使用稍微更改的API:

template_((class A, class B, class C),
    requires(IsFoo<A> && IsBar<B>)
    requires(IsBaz<C>)
)
void frobozzle(A, B, C);

它输出了所希望的内容:

template<class A, class B, class C , 
  int dummy = 0 , 
  std::enable_if_t< dummy == 0 && (IsFoo<A> && IsBar<B>) > = 0 ,
  std::enable_if_t< dummy == 0 && (IsBaz<C>) > = 0 >
void frobozzle(A, B, C);

虽然不完全符合所请求的API,但优点是您可以使用需要宏来表达逗号,这要归功于使用VA_ARGS

template_((class A, class B, class C),
    requires(IsBaseOf<B,C>)
)
int boo()

我努力尝试制作一个无限的FOLD_LEFT,但似乎达不到我的要求 :D。

我没有尝试过使用表达式模板来复制您提供的相同输入语法,这听起来对我来说更可行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接