如何在 cpp 中实现可读的模板 for 循环语法?

How to implement a readable template for-loop syntax in cpp?

提问人: 提问时间:8/23/2023 更新时间:8/23/2023 访问量:149

问:

我想编写一些头文件,其中包含这些文件允许我以可访问的语法编写模板 for 循环。我该如何实现?(参见底部为我最好的尝试)。

下面是一个示例和标准,说明可访问语法的含义。

例:

for_loop<int,i0,ic,iE>(i){ // for(int i=i0;i<iE;i+=ic), where i0,ic,iE are constexpr int
   foo(i);
   bar<i>();
   for_loop<int,i,1,10>(j){
      boo<i,j>();
   }
}

标准

正如该示例所满足的,可访问语法的有用条件是:

  • 每个 for 循环的范围都有支撑
  • 没有样板(如创建 lambda 函数、函子类、索引序列、令人困惑的工件,如 、 ,它们既增强了循环范围的可读性,也没有增强循环体的可读性等)()[],...
  • 在支撑作用域内变量和函数的可访问性与经典的 for 循环相同。
  • desirable:使用 as 的表示法将它们指示为模板参数。// 使用 (i 是因为我假设我们需要一个宏来表示符号 i<>int,i0,ic,iE

相关问题如下:

  • [https://stackoverflow.com/questions/13816850/is-it-possible-to-develop-static-for-loop-in-c]
  • [https://stackoverflow.com/questions/1032602/template-ing-a-for-loop-in-c]
  • [https://stackoverflow.com/questions/42019564/how-to-iterate-over-stdindex-sequence]

然而,这些问题的重点是功能的实现。相反,这个问题的重点明确地放在实现可访问的语法上。

最佳尝试

以下是我为实现解决方案而做出的最佳功能尝试:

# define template_for_loop_begin(index) \
    [&]<std::size_t... index>(std::index_sequence<index...>) {

# define template_for_loop_end(index,N) ;}(std::make_index_sequence<N>{});

// this allows writing a template for-loop as follows:
template_for_loop_begin(i)(
  bar<i>(),...
)template_for_loop_end(i,10)

但是,生成的语法并不令人满意。问题是:

  • Major:循环体限制为一行
  • major:循环体必须是单个函数调用
  • semi:有,而不是在行的末尾,...;
  • semi:索引位于循环的页脚中
  • 次要的环括号代替了(){}

Normal For 循环示例及其带来的问题

真实世界的代码示例是以下客户的代码,使用我的样条库。

for(int i=0;i<1000;++i){
  int j=i;
  spline.load(j,data[j]);
}

这工作正常。现在一些员工已经有了坏主意,比如:

for(int i=0;i<500;++i){
  if(data[i]>0){ j=i+500;}else{j=i;}
  spline.load(j,data[i]);
}

我的软件使用基于生成源代码的运行时的自动代码转换。为了使此源代码转换产生我的预期行为,索引必须是一个 constexpr。因此,我想把它做成模板。但是,我无法教育客户重新学习一种编写 for 循环的方式,从而阻止/抑制他们使用我的代码。这就引出了这个问题。j

模板、Consteval 和 Consteffr 以及类型

感谢 Yakk - Adam Nevraumont 提供的善意答案,我们可以使用以下内容:static_foreach

#include <utility>
template<auto first, auto increment, auto limit>
struct static_foreach_t {
  void operator->*( auto f ) const& {
     [&]<auto...Is>(std::integer_sequence<decltype(first), Is...>) {
       ( f( std::integral_constant<decltype(Is), first+Is*increment>{} ), ...);
     }( std::make_integer_sequence< decltype(first), (limit-first)/increment >{} );
  }
};
template<auto first, auto increment, auto limit>
constexpr static_foreach_t<first, increment, limit> static_foreach = {};

// ++++

template<typename T>
struct Cxpr{
    const T t;
    consteval Cxpr(T t):t(t){}
};

void foo(int){}
void bar(Cxpr<int>){}
template<int> void boo(void){}

int main(){
    static_foreach<3, 2, 7>->*[&](auto i) {
        foo(i);
        //bar(i);
        boo<i>();
    };
}

但是,代码在取消注释时会抛出错误,这是一个 consteval。这能解决吗?我尝试了各种构造函数和 assigment 重载。另一个破坏游戏规则的是它不适用于浮点数和双精度,例如,在我的样条库中,还有一些函数只有在浮点数可以断言为 constexpr/consteval 类型时才能防止误用。 error: could not convert 'i' from 'std::integral_constant<int, 3>' to 'Cxpr<int>'bar(i);Cxpr

C++ 模板 lambda variadic-templates

评论

2赞 tadman 8/23/2023
A) 为什么?B) 为什么???C)这如何比常规的C++循环更“容易访问”,包括基于范围的范围,老实说,对于C++来说是一个令人惊讶的干净设计?
0赞 8/23/2023
因为客户在将 spline.load<index>(data) 用于许多索引时面临困难,这会损害业务。
1赞 tadman 8/23/2023
“客户”?“业务”?你想在这里解决什么问题?您尝试简化的代码的一个很好的示例是什么?
1赞 Pete Becker 8/23/2023
请举一个普通的 C++ for 循环的例子,它执行您模糊描述的内容。然后解释它带来了什么问题。
1赞 8/23/2023
@tadman:如果我理解正确的话,我用皮特和你要求的例子来扩展这个问题。这有帮助吗?

答:

1赞 Yakk - Adam Nevraumont 8/23/2023 #1
for_loop<int,i0,ic,iE>(i){// for(int i=i0;i<iE;i+=ic), where i0,ic,iE are constexpr int

这是一个简单的语法:

for( int i:range<i0,ic,iE> ) {
  // body
}

但我怀疑这是由于您选择不提及的限制(例如,想要在上下文中使用)而失败的。iconstexpr

要做到这一点,您将需要一个对用户隐藏的 lambda。

无宏版本可能如下所示:

foreach<first, increment, limit>->*[&](auto i) {
};

名称的宏注入要求将宏扩展到该位置。i

#define FOREACH( FIRST, INCREMENT, LIMIT, NAME ) \
  foreach FIRST, INCREMENT, LIMIT ->* [&](auto NAME)

用户在哪里做

// Note the <> below:
FOREACH( <i0, ic, iE>, i ) {
  // function body
}; //note the ; requirement

它如上所述扩展。我发现这是一个坏主意,因为如果它编译失败,你会得到一堆无意义的错误消息,用户不知道为什么会发生这种情况,可能涉及预处理器问题,也可能在生成的代码中。

诀窍是,这是一个带有重载运算符的模板值,该运算符在右侧采用可调用对象。->*foreach<first, increment, limit>->*

template<auto first, auto increment, auto limit>
struct static_foreach_t {
  void operator->*( auto f ) const&& {
     [&]<auto...Is>(std::index_sequence<Is...>) {
       ( f( std::integral_constant<decltype(Is), first+Is*increment>{} ), ...);
     }( std::make_integer_sequence< decltype(first), (limit-first)/increment >{} );
  }
};

template<auto first, auto increment, auto limit>
constexpr static_foreach_t static_foreach = {};

所以,忽略你的要求,我不喜欢:

static_foreach<1, 5, 11>->*[&](auto i) {
  int array[i] = {0};
};

或者,使用宏(坏主意):

#define STATIC_FOREACH(FIRST,INCREMENT,LIMIT,NAME) \
  static_foreach FIRST, INCREMENT, LIMIT ->*[&](auto NAME)

STATIC_FOREACH( <1, 5, 11>, i) {
  int array[i] = {0};
  std::cout << "[";
  for (auto e : array)
    std::cout << e;
  std::cout << "]\n;
};

或者在我的首选版本中:

static_foreach<1, 5, 11>->*[&](auto i) {
  int array[i] = {0};
  std::cout << "[";
  for (auto e : array)
    std::cout << e;
  std::cout << "]\n";
};

虽然可能是一堆魔术代币,但根据我的经验,将所述魔术代币隐藏在宏后面并不能使事情变得更友好->*[&](auto i)

最终用户不会理解宏或魔术令牌,但宏意味着当宏产生错误时,最终用户将没有希望找出出了什么问题。同时,如果魔术标记产生错误,你会得到实际的编译器的帮助。

现场示例

哦,还有一个非常大的问题。循环主体中的 A 不会执行用户期望的操作。returnreturn

让函数调用和 lambda 可见会让这变得不那么令人惊讶。

消除这个弱点是不可能的,因为你不能让你在 C++ 中调用的函数控制调用它的代码的返回流,除非有一些非常奇怪的协程魔术情况。


现在的类型不是 .我们可以用更多的愚蠢来解决这个问题:iconstexpr int

template<auto first, auto increment, auto limit>
struct static_foreach_t {
  void operator->*( auto f ) const& {
     [&]<auto...Is>(std::integer_sequence<std::size_t, Is...>) {
       ( f.template operator()<first+Is*increment>(), ...);
     }( std::make_integer_sequence<std::size_t, (limit-first)/increment >{} );
  }
};

template<auto first, auto increment, auto limit>
constexpr static_foreach_t<first, increment, limit> static_foreach = {};

#define STATIC_FOREACH(FIRST,INCREMENT,LIMIT,NAME) \
  static_foreach FIRST, INCREMENT, LIMIT ->*[&]<auto NAME>()

在这里,我们没有传递 a ,而是将值作为模板参数传递给 。std::integral_constantoperator()

在非宏情况下使用稍作更改:

static_foreach<1, 5, 11>->*[&]<auto i>() {
  int array[i] = {0};
  std::cout << "[";
  for (auto e : array)
    std::cout << e;
  std::cout << "]\n";
};

因为我们把非类型参数作为模板。(可以阅读或其他什么)。iauto iint i

对于宏情况,它看起来是相同的:

STATIC_FOREACH( <1, 5, 11>, i ) {
  int array[i] = {0};
  std::cout << "[";
  for (auto e : array)
    std::cout << e;
  std::cout << "]\n";
};

对循环的非整数值的支持需要稍作调整:

template<auto first, auto increment, auto limit>
struct static_foreach_t {
  void operator->*( auto f ) const& {
     [&]<auto...Is>(std::integer_sequence<std::size_t, Is...>) {
       ( f.template operator()<first + Is*increment>(), ...);
     }( std::make_integer_sequence< std::size_t, static_cast<std::size_t>((limit-first)/increment) >{} );
  }
};

这在实验上适用于浮子

评论

0赞 8/23/2023
谢谢。下次,我怎么能把要求说得更清楚?我试图通过术语“模板”来阐明,并在示例中用 .ibar<i>();
0赞 8/23/2023
@YakkAdamNevraumont:当您的解决方案与 consteval 包装器结合使用时,我遇到了一个半问题,因为 std::integral_constant<int, 3> 无法转换为 constexpr int 或 consteval int(实际,Cxpr<int>,参见 attendum to question)。这种情况可以缓解吗?
0赞 Yakk - Adam Nevraumont 8/23/2023
@violetvanillavendetta 这只是双用户转换问题。投射到喜欢,或做。我可以跳过重重障碍,成为一个真正的模板常数; 诸如此类的事情。操作员甚至可以改用。intbar((int)i)bar(i())i[&]<auto i>(std::integral_constant<int, i>)->*template operator()<Is>