在 C++ 中使用宏实现具有自己的循环计数器的 for 语句

Use macros to implement a for statement with its own loop counter in C++

提问人:Jemtaly 提问时间:10/30/2023 最后编辑:Jemtaly 更新时间:10/31/2023 访问量:194

问:

我的要求是计算程序循环中每个for语句在程序的整个运行时的总次数,例如:

int for_counter_1 = 0;
int for_counter_2 = 0;

void f() {
    for (int i = 0; i < 10; i++, for_counter_2++) {
        // ...
    }
}
int main() {
    for (int i = 0; i < 10; i++, for_counter_1++) {
        f();
    }
    printf("%d\n", for_counter_1); // should output: 10
    printf("%d\n", for_counter_2); // should output: 100
    
}

由于我的程序中有非常多的 for 循环(一些加密算法),并且我将继续扩展它,因此我考虑使用宏来实现具有自己的循环计数器的 for 语句,如下所示:

#define CONCAT_INNER(a, b) a ## b
#define CONCAT(a, b) CONCAT_INNER(a, b)
#define FOR_COUNTER() CONCAT(for_counter_, __LINE__)
#define for_with_counter(...) \
    static int FOR_COUNTER() = 0; \
    for (__VA_ARGS__, ++FOR_COUNTER())

void f() {
    for_with_counter(int i = 0; i < 10; ++i) {
        // ...
    }
}
int main() {
    for_with_counter(int i = 0; i < 10; ++i) {
        f();
    }
}

它可以为每个 for 循环生成一个静态计数器,其中 N 表示循环所在的行号。我可以看到在调试环境中发生此循环的总次数。但是,由于它们是局部静态变量,因此我无法在程序结束时从主函数中将它们全部打印出来。for_counter_N

有什么方法可以实现我的目的吗?它不一定是宏,而且由于我使用的是 C++,如果我可以使用模板等来绕过它就好了。或者,如果有一个调试工具可以做到这一点,那也可以工作。

编辑:

对不起,我没有指定,这并不是说我没有考虑过使用 map 或 unordered_map,但是由于我的程序中 for 循环的性能要求非常苛刻(因为我正在做一些加密算法优化方面的工作),使用这些数据结构会产生一些性能影响。

C++ 调试 C-Preprocessor

评论

1赞 Some programmer dude 10/30/2023
我建议你从类似 Optionally use a for the counters 开始,如果你知道编译时需要的计数器数量。然后包装在一个类中。std::unordered_map<unsigned, unsigned> counters; /* ... */ counters[1]++; /* ... */ for (auto const& counter : counters) { std::cout << "Counter " << counter.first << " have the value " << counter.second << '\n'; }std::array
0赞 Some programmer dude 10/30/2023
当然,如果您想为计数器提供更相关的名称,而不仅仅是索引,则可以使用带有键的映射。std::string
0赞 Jesper Juhl 10/31/2023
我建议使用函数而不是宏。

答:

4赞 HolyBlackCat 10/30/2023 #1

使用全局计数器,将 和 组合为键(例如键)。map__FILE____LINE__std::pair<std::string_view, int>

评论

2赞 Marek R 10/31/2023
有 C++20 std::source_location::current
2赞 Jarod42 10/30/2023 #2

模板功能似乎更好:

extern std::/*unoredered_*/map<std::string_view, std::size_t> loop_counters; // put definition in cpp file.

// F callable which return false to stop the loop
template <typename F>
void for_with_counter(std::string_view loop_name, int begin, int end, F f) {
    auto& counter = loop_counters[loop_name];
    for (int i = begin; i != end; ++i) {
        ++counter;
        if constexpr (std::is_same_v<void, decltype(std::invoke(f, i))>) {
             std::invoke(f, i);
        } else {
            if (!std::invoke(f, i)) {
                return;
            }
        }
    }
}

用法

for_with_counter("my_loop", 0, 10, [/*..*/](int i){ /* .. */ });

未明确提供名称的版本:

template <typename F>
void for_with_counter(int begin, int end, F f, std::source_location loc = std::source_location::current()) {
    auto name = std::string(loc.function_name()) + " " + loc.file_name() + ":" + std::to_string(loc.line());
    for_with_counter(name, begin, end, f);
}

并将地图更改为 AS 键。std::string

演示

评论

0赞 Red.Wave 10/31/2023
为什么你总是比我早十分钟?你的思想窃贼😉,这个答案值得点赞。但是你必须解决两件事:1.为可中断的循环提供一个示例,为牢不可破的循环提供一个示例 - 前者必须模拟。2. 修改代码以接受 C++20 范围而不是整数边界 - 整数边界可以简单地使用 模拟。基本上,我要求你实现(可中断循环)和(不中断循环)的分析对应物;实现正在转发到未分析的对应项。continuestd::views::iota(st,ed)std::ranges::find_ifstd::ranges::for_eachranges
5赞 Pepijn Kramer 10/30/2023 #3

只要有可能,我就会尽量完全远离宏。 在本例中,我将创建一个类型,其行为类似于 ,但会跟踪它递增的频率。对于此演示 (https://onlinegdb.com/zTJqjvNvE),报告位于析构函数中(因此,当计数器超出范围时)。int

我的想法是,你不需要知道循环的类型,只需要知道某件事的递增频率(这种方法也适用于while循环)

#include <string>
#include <iostream>

template<typename type_t>
class counter_t
{
public:
    explicit counter_t(const std::string name) :
        m_name{ name },
        m_count{ 0ul }
    {
    }

    auto& operator=(const type_t& value)
    {
        m_value = value;
        return *this;
    }

    void operator++()
    {
        m_value++;
        m_count++;
    }

    void operator++(int)
    {
        ++m_value;
        ++m_count;
    }

    operator const type_t& () const
    {
        return m_value;
    }

    operator type_t& ()
    {
        return m_value;
    }

    ~counter_t()
    {
        std::cout << "counter : `" << m_name << "` was incremented " << m_count << " times\n";
    }

private:
    std::string m_name;
    type_t m_value;
    std::size_t m_count;
};

int main()
{
    {
        counter_t<int> counter1{ "counter1" };

        for (counter1 = 1; counter1 < 10; ++counter1)
        {
            if (counter1 > 5) break;
        }
    } // go out of scope and report

    return 1; // can set breakpoint here
}
2赞 Jerry Coffin 10/31/2023 #4

我想我会像其他人建议的那样使用全局映射,通过创建一个包装类来初始化对其 ctor 中计数器对象的引用来避免它产生重大开销,所以从那时起,它只是做几个正常的增量,就像你的代码一样。

如果你真的愿意,你甚至可以通过创建一个局部变量,在每次迭代中更新它,然后将其添加到 dtor 中的全局变量来避免在每次迭代中使用引用的潜在开销(但对我来说,这似乎是相当多的工作,不太可能完成太多工作)。

对于从 0 开始每个循环并始终递增 1 的典型情况,只需在每次循环迭代中递增局部变量,然后将其添加到 dtor 中的全局计数器中,就可以进一步减少开销。

一方面,这确实可能会在进入循环时增加开销。另一方面,它可能会减少循环每次迭代的开销。

下面是概念验证级别代码的快速部分:

#include <map>
#include <iostream>
#include <source_location>

class LoopCounterImpl {
    static std::map<int, int> counters;
    int &globalCounter;
    int counter;
public:
    LoopCounterImpl(int initial_val, int loc)
        : counter(initial_val), globalCounter(counters[loc])
    {}

    LoopCounterImpl &operator++() {
        ++counter;
        return *this;
    }

    void operator++(int) { 
        ++counter;
    }

    operator int() const { return counter; }

    static void dump(std::ostream &os = std::cout) {
        os << "\n\n" << __FILE__ << ":\n";
        for (auto const &rec : counters) { 
            os << "line: " << rec.first << ", count: " << rec.second << "\n";
        }
    }

    ~LoopCounterImpl() { globalCounter += counter; }
};

std::map<int, int> LoopCounterImpl::counters;

#define LoopCounter(x) LoopCounterImpl(x, __LINE__)

// And a quick demo of using it:
int main() { 
    for (auto i = LoopCounter(0); i<10; ++i) {
        std::cout << i << " ";
    }
    std::cout << "\n";

    for (auto j = LoopCounter(0); j<20; j++) {
        std::cout << j << " ";
    }

    LoopCounterImpl::dump();
}

为了将开销保持在最低限度,我编写了此操作,使其后增量运算符仅将前一个值作为 返回,其中它通常应返回 的副本(作为临时对象)。但是在循环中,通常只执行 a 来增加变量的副作用,而不关心返回值。使用临时 LoopCounter 所能做的就是检索其当前值,因此我刚刚返回了一个以最大程度地减少开销。int*thisLoopCounterImplfoo++intint

目前,它使用宏来自动检索 的正确值。反过来,这在某种程度上迫使你使用类似的东西,这对我来说似乎并不可怕,但我想有些人可能不喜欢。如果你不介意这样的事情,你可以放弃它。LoopCounter__LINE__auto foo = LoopCounter(initialValue)LoopCounter x{0, __LINE__}

显然,您还可以添加 、 、 等内容。我猜从已经存在的东西来看,其中大部分是很明显的。operator+=-=--

如果你打算把它放到一个标题中,你需要宏来收集 和 ,并将两者用作映射中的键。LoopCounter__FILE____LINE__