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

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

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())
{
  // 只在每个唯一的位置执行一次
  std::cout << location.file_name()
            << " (" << location.line() << ")"
            << std::endl;

  // 但是重复执行这部分
  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

最好将所有内容都封装在一个函数调用中,这样我就不需要在调用点周围添加静态变量。我尝试将所有内容封装在一个模板函数中:

template<typename UniqueT, typename Func>
void call_once(Func& func)
{
    func();
}

但是我找不到一种方法来为每个调用点生成唯一的类型。我想也许可以为std::source_location生成一个唯一的哈希值,但我不确定如何实现(因为source_location是一个默认参数,并且无论如何,我不知道是否可以在编译时计算它)。我研究了std::call_once,但这要求我持有一个标志,这个标志可以是静态的,但它会阻止函数的多次执行。至少我目前的理解是这样的。

如果可能的话,我不想在某个地方保存std::unordered_map,而且如果可以在编译时发生就更好了。

英文:

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)假设file_name的大小有界限(在编译时已知)。

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

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

template<Location location> // <-- Location类型稍后会出现。
void Bar() {
    static Runner once{[]() {
        // 仅在首次调用Bar<location>时执行一次的代码。
        std::cout << location.file_name() << '(' << location.line() << ')';
        std::cout << "在每个位置上执行一次的Bar<location>调用" << std::endl;
    }};

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

关于这个实现的一些事情(可能是期望的或不期望的):

  • static Runner once的初始化是线程安全的。
  • 静态初始化是阻塞的:其他Bar<someLoc>调用在成功运行“一次”部分之前不会运行“每次”部分。
  • 如果Runner构造函数抛出异常,Bar的其他调用将尝试再次运行“一次”部分。
  • 使用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 << "来自'" << location.function_name() << "': ";
    std::cout << message << '\n';
}

template<LocationSize ls, Location<ls> location>
void Bar()
{
    static Runner once{[]() {
        // 仅在每个唯一位置执行一次的代码
        log(location, "一次部分");
    }};

    // 但是重复执行这个
    log(location, "重复部分");
}

void Foo()
{
    std::cout << "Foo调用" << std::endl;
    constexpr auto loc = here();
    Bar<loc,loc>();
}

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

在线演示(godbolt)

template<LocationSize ls, Location<ls> location>中发生了魔法。这两种类型都可以从std::source_location转换而来,并且应该使用相同的source_location调用两次。第一次转换为LocationSize ls提取字符串的大小,只是计算第二个参数的确切类型的中间值。

在主函数中调用Bar<here(),here()>()有点巧妙,因为我们传递了两个不同的source_location。但是,它应该仍然工作,因为真正重要的是它们具有相同的file_name/function_name字符串。而且语法比声明一个constexpr变量要紧凑得多。

警告:二进制大小

在每个调用点上实例化了不同的Bar模板,导致常见的汇编代码重复。此外,每个Bar实例化都会将模板参数location作为常量存储在程序内存中,每个常量都有其自己的file_name/function_name副本。根据您的构建过程,file_name可能是源文件的完整绝对路径...

这不是零成本的。

英文:

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:

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.html
匿名

发表评论

匿名网友

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

确定