0

I am writing a log function that takes a format string and a variable number of arguments and captures std::source_location. This is not straightforward, so I tried the FormatWithLocation solution described in How to use source_location in a variadic template function? - which works.

Now, I want to process std::source_location::function_name() - at compile time. And I don't really see a way to create a compile-time string and use it at runtime without actually hardcoding the string.

This is what I tried (in a much simplified version which doubles each charater in the input function name). I resorted to a bunch of tricks, including using std::array for compile-time strings, but for those I need to know the size in advance; with any other data structure, I cannot seem to use compile-time heap-allocated data in the run-time world (understandably).

#include <array>
#include <iostream>
#include <source_location>
#include <string>

// TODO: avoid hardcoded size
constexpr size_t N = 61;

struct StringWithSourceLocation {
    template <size_t inputLength>
    static consteval std::array<char, inputLength> toArray(
        const char* const inputText) {
        std::array<char, inputLength> array{};
        for (size_t i = 0; i < inputLength; i++) array[i] = inputText[i];
        return array;
    }

    template <size_t inputLength>
    static consteval size_t computeLength(
        const std::array<char, inputLength>& inputText) {
        return inputLength * 2 + 1;
    }

    template <size_t inputLength, size_t outputLength>
    static consteval std::array<char, outputLength> produceOutput(
        const std::array<char, inputLength>& inputText) {
        std::array<char, outputLength> outputText{};
        size_t outputIndex = 0;
        for (const char c : inputText) {
            outputText[outputIndex++] = c;
            outputText[outputIndex++] = c;
        }
        outputText[outputIndex++] = 0;
        return outputText;
    }

    static consteval std::array<char, N> process(
        const std::source_location sourceLocation) {
        // TODO: work without quotation marks
        constexpr const char* functionName = "sourceLocation.function_name()";
        constexpr size_t inputLength =
            std::string::traits_type::length(functionName);

        constexpr auto input = toArray<inputLength>(functionName);
        constexpr size_t outputLength = computeLength(input);

        constexpr std::array<char, N> outputText =
            produceOutput<inputLength, outputLength>(input);
        return outputText;
    }

    consteval StringWithSourceLocation(
        const char* message,
        const std::source_location sourceLocation =
            std::source_location::current(),
        const std::array<char, N> processedOutputName =
            process(std::source_location::current()))
        : message(message),
          sourceLocation(sourceLocation),
          processedFunctionName(processedOutputName) {}

    const char* message;
    const std::source_location sourceLocation;
    const std::array<char, N> processedFunctionName;
};

static void log(const StringWithSourceLocation foo) {
    // This shall return a compile-time result:
    std::cout << foo.processedFunctionName.data() << ": " << foo.message
              << std::endl;
}

int main() { log("bar"); }

Output:

ssoouurrcceeLLooccaattiioonn..ffuunnccttiioonn__nnaammee(()): bar

So the code above works, but you see two TODOs in this code:

  1. Somehow, the hardcoded size needs to be converted into a template parameter of the class which is deduced from the input source_location.
  2. The input source_location should actually be used in processing, instead of the hardcoded string.

Is this possible at all?

(This is for MSVC 17.10, if this is relevant at all.)

2 Answers 2

3

Not sure it will fit what you require but here is a proposal for getting a constexpr array holding the function name.

First, you can define a getFunctionNameAsArray function that builds a std::pair holding

  1. a std::array of char of a given max size (NMAX provided as a non type template parameter) that will hold the function name provided as a default argument of the function. In case the length of the function name is too big, an exception is thrown.
  2. the actual size of the function name
template<std::size_t NMAX=256>
static constexpr auto getFunctionNameAsArray (const char* fctname = std::source_location::current().function_name()) {
    return  [&] {
        std::array<char,NMAX> res = {};
        std::size_t len=strlen(fctname);
        if (len>=NMAX)  { throw std::runtime_error("bad length"); }
        std::size_t i=0;
        for (; i<len; ++i)  { res[i] = fctname[i]; }
        return std::make_pair(res,i);
    } ();
}

Next, you can define some foo function that will return the array truncated to the correct size. For that, we provide as non-type template parameter (c++20 required) the result of getFunctionNameAsArray, which allows to use the actual size when declaring the resulting array.

template<auto info=getFunctionNameAsArray()>
static constexpr auto foo ()  {
    return  [&] {
        std::array<char,info.second> res = {};
        for (std::size_t i=0; i<info.second; ++i)  { res[i] = info.first[i]; }
        return res;
    } ();
}

One can check that we get something at compile time

int main()  {
    static_assert (foo() == std::array<char,10> {'i','n','t',' ','m','a','i','n','(',')'} );
    return 0;
}

Demo


As a variation, one can define foo with the help of std::index_sequence and pack expansion

template<auto info=getFunctionNameAsArray()>
static constexpr auto foo ()  {
    return  [] <std::size_t...Is> (std::index_sequence<Is...> ) {
        return std::array<char,info.second> { info.first[Is]... };
    } (std::make_index_sequence<info.second>());
}
Sign up to request clarification or add additional context in comments.

2 Comments

Interesting trick! I tried setting the compiler to MSVC to see if it's applicable to me, and unfortunately, it fails to compile. I was able to fix it by defining my own consteval mystrlen, and I had to remove the lambda trick (what is this one for, actually?). Now, unfortunately, the result of foo() seems to be array<char, 0> :( godbolt.org/z/zfMbz9xjz
Turns out you cannot use std::source_location::current().function_name() as a default argument in MSVC. Use std::source_location::current() and call function_name() in the function body: godbolt.org/z/z9oWj5v3c
1

Hmm...Sounds a bit tricky, but what if you will do your StringWithSourceLocation as a template class with minimal code in it and instead will delegate this job to other some method, for instance

template <size_t N>
static void log_impl(const char* message,
                     std::source_location location,
                     const char (&funcName)[N]) {
    constexpr auto processed = double_chars(funcName);
    log(StringWithSourceLocation(message, location, processed));
}

Instead of static void log(const StringWithSourceLocation foo)... you will use macros wrapper on it that will invoke your StringWithSourceLocation and apply specific method that you want to pass, in our case

#define LOG(msg)                                                       \
    ([&] {                                                             \
        constexpr auto name = double_chars(__FUNCTION__);              \
        return StringWithSourceLocation(msg, std::source_location::current(), name); \
    }())

1 Comment

Would this call double_chars() at compile time?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.