执行一段代码块一次,每次在std::source_location调用发生的地方。

huangapple go评论76阅读模式
英文:

Function that executes a block of code once per std::source_location where the invocation takes place

问题

我正在尝试编写一个可以从许多不同地方调用的函数。该函数的一部分应该只在每个文件名+行号的组合上发生一次。

#include <iostream>
#include <source_location>

void Bar(const std::source_location& location = std::source_location::current())
{
  // 只执行一次的部分
  static bool executed = false;
  if (!executed)
  {
    std::cout << location.file_name()
              << " (" << location.line() << ")"
              << std::endl;
    executed = true;
  }

  // 但是这部分会重复执行
  std::cout << "call multiple times" << std::endl;
}

void Foo()
{
  std::cout << "Called" << std::endl;
  Bar();
}

int main()
{
  Bar();
  Foo();
  Foo();
  return 0;
}

期望的输出是:

example.cpp (23)
call multiple times
Called
example.cpp (18)
call multiple times
Called
call multiple times

这里我在函数中添加了一个静态布尔变量 executed 来确保只有第一次调用时才会执行该部分。这样,您不需要在调用站点周围散布静态变量。

希望这能帮助您实现所需的功能。

英文:

I'm trying to write a function that will be called from lots of different places. Part of this function should only occur once, but "once" per filename + linenumber making the invokation.

#include &lt;iostream&gt;
#include &lt;source_location&gt;

void Bar(const std::source_location&amp; location = std::source_location::current())
{
  // only do this once per unique location
  std::cout &lt;&lt; location.file_name()
            &lt;&lt; &quot; (&quot; &lt;&lt; location.line() &lt;&lt; &quot;)&quot;
            &lt;&lt; std::endl;

  // but do this repeatedly
  std::cout &lt;&lt; &quot;call multiple times&quot; &lt;&lt; std::endl;
}

void Foo()
{
  std::cout &lt;&lt; &quot;Called&quot; &lt;&lt; std::endl;
  Bar();
}

int main()
{
  Bar();
  Foo();
  Foo();
  return 0;
}

https://godbolt.org/z/jhba98cPK (compiles under GCC 13.3 with -std=c++23)

Current output:

example.cpp (23)
call multiple times
Called
example.cpp (18)
call multiple times
Called
example.cpp (18)
call multiple times

What I want:

example.cpp (23)
call multiple times
Called
example.cpp (18)
call multiple times
Called
call multiple times

Preferably it's all rolled into a single function call so that I don't need to litter static variables around the calling site. I tried wrapping this all up into a templated function:

template&lt;typename UniqueT, typename Func&gt;
void call_once(Func&amp; func)
{
    func();
}

but I can't find a way to generate a unique type per calling site. I thought maybe I could generate a unique hash per std::source_location, but I'm not sure how (because the "source_location" is a default parameter, and in any case, I don't know if I can calculate it at compile time). I looked into std::call_once but this requires that I hold a flag, which I guess can be static, but it would prevent the multiple-part of the function from occurring. At least as near as I can figure.

I'd prefer not to hold an std::unordered_map etc somewhere, and would much prefer it occurred compile-time if it was at all possible.

答案1

得分: 1

情况1)假设文件名具有已知大小(在编译时已知)。

基本思想是将 source_location 作为 Bar 的模板参数而不是函数参数传递。 Bar 可以重写如下:

struct Runner {
    Runner(auto&& callable) { callable(); }
}

template<Location location> // <-- Location 类型稍后传递。
void Bar() {
    static Runner once{[]() {
        // 仅执行一次的代码。
        std::cout << location.file_name() << '(' << location.line() << ')';
        std::cout << "每次在第1次 Bar<location> 调用时执行一次" << std::endl;
    }};

    // 多次运行的代码。
    std::cout << "每次都会执行" << std::endl;
}

关于此实现的一些事项(可能有所希望或不希望的):

  • static Runner once 初始化是线程安全的。
  • 静态初始化是阻塞的:其他 Bar<someLoc> 调用在一个 Bar<someLoc> 调用成功运行 "once" 部分之前不会运行 "each time" 部分。
  • 如果 Runner 构造函数抛出异常,其他的 Bar 调用将尝试再次运行 "once" 部分。
  • 使用静态的 std::once_flagstd::call_once 应该具有相同的可观察行为(然而,生成的汇编在 gcc 上似乎不太优化)。

模板参数不能直接是类型 std::source_location,所以我们需要定义一个自定义的 Location 类型以传递为 NTTP。首先让我们创建一个字符串类,它可以用作 NTTP 而不是 const char*(这些 constexpr_string/fixed_string/sized_string 是常见的 C++ 元编程技巧):

template<std::size_t capacity>
struct ConstexprString {
public:
    [[nodiscard]]
    consteval ConstexprString(char const* source)
        : size_{ 0 }
    {
        // 基本的 C 字符串函数不是 constexpr。重新发明轮子...
        while (source[size_] != '
template<std::size_t capacity>
struct ConstexprString {
public:
    [[nodiscard]]
    consteval ConstexprString(char const* source)
        : size_{ 0 }
    {
        // 基本的 C 字符串函数不是 constexpr。重新发明轮子...
        while (source[size_] != '\0') {
            if (size_ >= capacity) {
                throw std::invalid_argument("输入字符串不适合。");
            }
            content_[size_] = source[size_];
            ++size_;
        }
        // constexpr 值不能具有未初始化的字节。
        if (capacity > size_) {
            std::ranges::fill_n(content_ + size_, capacity - size_, '\0');
        }
    }

    [[nodiscard]]
    constexpr std::string_view view() const {
        return { content_, size_ };
    }

    // 为了将此类用作 NTTP,成员变量不能是私有的。
    std::size_t size_;
    char content_[capacity]; // 不一定是 '\0' 结尾的。
};
'
) {
if (size_ >= capacity) { throw std::invalid_argument("输入字符串不适合。"); } content_[size_] = source[size_]; ++size_; } // constexpr 值不能具有未初始化的字节。 if (capacity > size_) { std::ranges::fill_n(content_ + size_, capacity - size_, '
template<std::size_t capacity>
struct ConstexprString {
public:
    [[nodiscard]]
    consteval ConstexprString(char const* source)
        : size_{ 0 }
    {
        // 基本的 C 字符串函数不是 constexpr。重新发明轮子...
        while (source[size_] != '\0') {
            if (size_ >= capacity) {
                throw std::invalid_argument("输入字符串不适合。");
            }
            content_[size_] = source[size_];
            ++size_;
        }
        // constexpr 值不能具有未初始化的字节。
        if (capacity > size_) {
            std::ranges::fill_n(content_ + size_, capacity - size_, '\0');
        }
    }

    [[nodiscard]]
    constexpr std::string_view view() const {
        return { content_, size_ };
    }

    // 为了将此类用作 NTTP,成员变量不能是私有的。
    std::size_t size_;
    char content_[capacity]; // 不一定是 '\0' 结尾的。
};
'
);
} } [[nodiscard]] constexpr std::string_view view() const { return { content_, size_ }; } // 为了将此类用作 NTTP,成员变量不能是私有的。 std::size_t size_; char content_[capacity]; // 不一定是 '
template<std::size_t capacity>
struct ConstexprString {
public:
    [[nodiscard]]
    consteval ConstexprString(char const* source)
        : size_{ 0 }
    {
        // 基本的 C 字符串函数不是 constexpr。重新发明轮子...
        while (source[size_] != '\0') {
            if (size_ >= capacity) {
                throw std::invalid_argument("输入字符串不适合。");
            }
            content_[size_] = source[size_];
            ++size_;
        }
        // constexpr 值不能具有未初始化的字节。
        if (capacity > size_) {
            std::ranges::fill_n(content_ + size_, capacity - size_, '\0');
        }
    }

    [[nodiscard]]
    constexpr std::string_view view() const {
        return { content_, size_ };
    }

    // 为了将此类用作 NTTP,成员变量不能是私有的。
    std::size_t size_;
    char content_[capacity]; // 不一定是 '\0' 结尾的。
};
' 结尾的。
};

然后我们可以定义我们的 NTTP 兼容类 Location

struct Location {
public:
    static constexpr std::size_t fileCapacity = 128;
    static constexpr std::size_t functionCapacity = 128;

    [[nodiscard]]
    consteval Location(std::source_location const& location)
        : column_{ location.column() }
        , line_{ location.line() }
        , fileName_{ location.file_name() }
        , functionName_{ location.function_name() }
    {}

    [[nodiscard]]
    constexpr std::string_view file_name() const {
        // 个人观点:在现代 C++ 中,string_view 感觉比 'const char*' 更 "正确"。
        return fileName_.view();
    }

    [[nodiscard]]
    constexpr std::string_view function_name() const {
        return functionName_.view();
    }

    [[nodiscard]]
    constexpr std::uint_least32_t column() const {
        return column_;
    }

    [[nodiscard]]
    constexpr std::uint_least32_t line() const {
        return line_;
    }

    // 为了将 Location 用作 NTTP,成员变量不能是私有的。
    std::uint_least32_t column_;
    std::uint_least32_t line_;
    ConstexprString<fileCapacity> fileName_;
    ConstexprString<functionCapacity> functionName_;
};

我们几乎完成了框架。用法:

// 最后的辅助函数
[[nodiscard]]
consteval std::source_location here(std::source_location const& loc = std::source_location::current()) {
    return loc;
}

void Foo()
{
  std::cout << "Called" << std::endl;
  Bar<here()>();
}

int main()
{
  Bar<here()>();
  Foo();
  Foo();
  return 0;
}

在线演示 (godbolt).

所有这些应该都是可移植的 C++20。

情况2)可变字符串容量。

Location 可以调整其字符串的容量。但是我的解决方案需要 Bar 接受2个模板参数:调用时更加冗长。

再次,让我们创建一个 NTTP 兼容的类来计算字符串的大小:

// std::strlen 不是 constexpr。重新发明轮子...
[[nodiscard]]
constexpr std::size_t StrLen(char const* str) {
    char const* cur = str;
    while (*cur != '
// std::strlen 不是 constexpr。重新发明轮子...
[[nodiscard]]
constexpr std::size_t StrLen(char const* str) {
    char const* cur = str;
    while (*cur != '\0') {
        ++cur;
    }
    return cur - str;
}

struct LocationSize {
public:
    [[nodiscard]]
    constexpr LocationSize(std::source_location const& location)
        : fileLength{ StrLen(location.file_name()) }
        , functionLength{ StrLen(location.function_name()) }
    {}

    std::size_t fileLength;
    std::size_t functionLength;
};
'
) {
++cur; } return cur - str; } struct LocationSize { public: [[nodiscard]] constexpr LocationSize(std::source_location const& location) : fileLength{ StrLen(location.file_name()) } , functionLength{ StrLen(location.function_name()) } {} std::size_t fileLength; std::size_t functionLength; };

Location 模板化以使用可变大小的字符串:

template<LocationSize sizes>
struct Location {
public:
    static constexpr std::size_t fileCapacity = sizes.fileLength;
    static constexpr std::size_t functionCapacity = sizes.functionLength;

    // 其余部分没有更改
};

用法:

void log(auto const& location, std::string_view message) {
    std::cout << location.file_name() << '(' << location.line() << ';' << location.column() << ')';
    std::cout << " from '" << location.function_name() << "

<details>
<summary>英文:</summary>

### Case 1) Assuming file_name has bounded size (known at compile time).

The fundamental idea is to pass the `source_location` as a template parameter of `Bar` instead of a function argument. `Bar` could be rewritten as such:

```c++
struct Runner {
    Runner(auto&amp;&amp; callable) { callable(); }
}

template&lt;Location location&gt; // &lt;-- Location type coming later.
void Bar() {
    static Runner once{[]() {
        // code to run once.
        std::cout &lt;&lt; location.file_name() &lt;&lt; &#39;(&#39; &lt;&lt; location.line() &lt;&lt; &#39;)&#39;;
        std::cout &lt;&lt; &quot;executed once per location on 1st Bar&lt;location&gt; call&quot; &lt;&lt; std::endl;
    }};

    // code to run multiple time.
    std::cout &lt;&lt; &quot;executed each time&quot; &lt;&lt; std::endl;
}

A few things about this implementation (that might be desirable or not):

  • static Runner once initialization is thread-safe.
  • static initialization is blocking: other Bar&lt;someLoc&gt; calls won't run the "each time" section until one Bar&lt;someLoc&gt; call has successfully run the "once" section.
  • in case the Runner constructor throws, other calls of Bar will attempt to run the "once" section again.
  • A static std::once_flag with a std::call_once should have the same observable behaviour (however the generated assembly seemed less optimized on gcc).

The template parameter can't directly be of type std::source_location, so we'll have to define a custom Location type to pass as NTTP. First let's make a string class that can be used as NTTP instead of const char* (these constexpr_string/fixed_string/sized_string are a common c++ metaprogramming trick):

template&lt;std::size_t capacity&gt;
struct ConstexprString {
public:
    [[nodiscard]]
    consteval ConstexprString(char const* source)
        : size_{ 0 }
    {
        // basic C str functions not constexpr. Reinventing the wheel...
        while (source[size_] != &#39;\0&#39;) {
            if (size_&gt;= capacity) {
                throw std::invalid_argument(&quot;input string does not fit.&quot;);
            }
            content_[size_] = source[size_];
            ++size_;
        }
        // constexpr values can&#39;t have uninitialized bytes.
        if (capacity &gt; size_) {
            std::ranges::fill_n(content_ + size_, capacity - size_, &#39;\0&#39;);
        }
    }

    [[nodiscard]]
    constexpr std::string_view view() const {
        return { content_, size_ };
    }

    // To use this class as a NTTP, member variable can&#39;t be private.
    std::size_t size_;
    char content_[capacity]; // not necessarily &#39;
template&lt;std::size_t capacity&gt;
struct ConstexprString {
public:
    [[nodiscard]]
    consteval ConstexprString(char const* source)
        : size_{ 0 }
    {
        // basic C str functions not constexpr. Reinventing the wheel...
        while (source[size_] != &#39;\0&#39;) {
            if (size_&gt;= capacity) {
                throw std::invalid_argument(&quot;input string does not fit.&quot;);
            }
            content_[size_] = source[size_];
            ++size_;
        }
        // constexpr values can&#39;t have uninitialized bytes.
        if (capacity &gt; size_) {
            std::ranges::fill_n(content_ + size_, capacity - size_, &#39;\0&#39;);
        }
    }

    [[nodiscard]]
    constexpr std::string_view view() const {
        return { content_, size_ };
    }

    // To use this class as a NTTP, member variable can&#39;t be private.
    std::size_t size_;
    char content_[capacity]; // not necessarily &#39;\0&#39; terminated.
};
&#39; terminated.
};

Then we can define our NTTP compatible class Location:

struct Location {
public:
    static constexpr std::size_t fileCapacity = 128;
    static constexpr std::size_t functionCapacity = 128;

    [[nodiscard]]
    consteval Location(std::source_location const&amp; location)
        : column_{ location.column() }
        , line_{ location.line() }
        , fileName_{ location.file_name() }
        , functionName_{ location.function_name() }
    {}

    [[nodiscard]]
    constexpr std::string_view file_name() const {
        // personal take: string_view feels more &quot;correct&quot; than &#39;const char*&#39; in modern c++.
        return fileName_.view();
    }

    [[nodiscard]]
    constexpr std::string_view function_name() const {
        return functionName_.view();
    }

    [[nodiscard]]
    constexpr std::uint_least32_t column() const {
        return column_;
    }

    [[nodiscard]]
    constexpr std::uint_least32_t line() const {
        return line_;
    }

    // To use Location as a NTTP, member variable can&#39;t be private.
    std::uint_least32_t column_;
    std::uint_least32_t line_;
    ConstexprString&lt;fileCapacity&gt; fileName_;
    ConstexprString&lt;functionCapacity&gt; functionName_;
};

We're almost done with the framework. Usage:

// last helper function
[[nodiscard]]
consteval std::source_location here(std::source_location const&amp; loc = std::source_location::current()) {
    return loc;
}

void Foo()
{
  std::cout &lt;&lt; &quot;Called&quot; &lt;&lt; std::endl;
  Bar&lt;here()&gt;();
}

int main()
{
  Bar&lt;here()&gt;();
  Foo();
  Foo();
  return 0;
}

Live demo (godbolt).

All of this should be portable c++20.

Case 2) Variable string capacity.

Location can be made to have just the right capacity for its strings. However my solution requires Bar to take 2 template parameters: calling it is more verbose.

Again, let's make a NTTP compatible class to compute the sizes of the strings:

// std::strlen isn&#39;t constexpr. Reinventing the wheel...
[[nodiscard]]
constexpr std::size_t StrLen(char const* str) {
    char const* cur = str;
    while (*cur != &#39;\0&#39;) {
        ++cur;
    }
    return cur - str;
}

struct LocationSize {
public:
    [[nodiscard]]
    constexpr LocationSize(std::source_location const&amp; location)
        : fileLength{ StrLen(location.file_name()) }
        , functionLength{ StrLen(location.function_name()) }
    {}

    std::size_t fileLength;
    std::size_t functionLength;
};

Templating Location to use variable sized strings:

template&lt;LocationSize sizes&gt;
struct Location {
public:
    static constexpr std::size_t fileCapacity = sizes.fileLength;
    static constexpr std::size_t functionCapacity = sizes.functionLength;

    // rest is unchanged
};

Usage:

void log(auto const&amp; location, std::string_view message) {
    std::cout &lt;&lt; location.file_name() &lt;&lt; &#39;(&#39; &lt;&lt; location.line() &lt;&lt; &#39;;&#39; &lt;&lt; location.column() &lt;&lt; &#39;)&#39;;
    std::cout &lt;&lt; &quot; from &#39;&quot; &lt;&lt; location.function_name() &lt;&lt; &quot;&#39;: &quot;;
    std::cout &lt;&lt; message &lt;&lt; &#39;\n&#39;;
}

template&lt;LocationSize ls, Location&lt;ls&gt; location&gt;
void Bar()
{
    static Runner once{[]() {
        // only do this once per unique location
        log(location, &quot;once section&quot;);
    }};

    // but do this repeatedly
    log(location, &quot;repeat section&quot;);
}

void Foo()
{
    std::cout &lt;&lt; &quot;Foo Call&quot; &lt;&lt; std::endl;
    constexpr auto loc = here();
    Bar&lt;loc,loc&gt;();
}

int main()
{
    Bar&lt;here(),here()&gt;();
    Foo();
    Foo();
    return 0;
}

Live demo(godbolt)

The magic is happening in template&lt;LocationSize ls, Location&lt;ls&gt; location&gt;. Both types are convertible from std::source_location and should be called with the same source_location passed twice. The first conversion to LocationSize ls extracts the string sizes, and is just an intermediate value to compute the exact type of the second parameter.

Calling Bar&lt;here(),here()&gt;(); in main is a bit hacky, since we're passing 2 different source_location. But it should work anyway since what really matters is that they have the same file_name/function_name strings. And the syntax is significantly more compact than declaring a constexpr variable.

Warning: binary size

A different Bar template is being instanciated at each call site, causing the usual assembly code duplication. On top of that, each Bar instanciation causes the template parameter location to be stored as a constant in the program memory, each with its own copy of file_name/function_name. Depending on your build process, file_name might be the complete absolute path of the source file...

This is NOT zero cost.

huangapple
  • 本文由 发表于 2023年8月9日 10:14:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/76864161-2.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定