7

I have very big code-base, which uses __FILE__ extensively for logging. However, it includes full path, which is (1) not needed, (2) might case security violations.

I'm trying to write compile-time sub-string expression. Ended up with this solution

static constexpr cstr PastLastSlash(cstr str, cstr last_slash)
{
    return *str == '\0' ? last_slash : *str == '/' ? PastLastSlash(str + 1, str + 1) : PastLastSlash(str + 1, last_slash);
}

static constexpr cstr PastLastSlash(cstr str)
{
    return PastLastSlash(str, str);
}

// usage
PastLastSlash(__FILE__);

This works good, I've checked assembly code, line is trimmed in compile time, only file name is present in binary.

However, this notation is too verbose. I would like to use macro for this, but failed. Proposed example from the link above

#define __SHORT_FILE__ ({constexpr cstr sf__ {past_last_slash(__FILE__)}; sf__;})

doesn't work for MSVC compiler (I'm using MSVC 2017). Is there any other method do to so using c++17?

UPD1: clang trimmed by function https://godbolt.org/z/tAU4j7

UPD2: looks like it's possible to do trim on compile time using functions, but full string is swill be present in binary.

8
  • 1
    I could not reproduce "This works good, I've checked assembly code, line is trimmed in compile time, only file name is present in binary." with newest GCC and clang: godbolt.org/z/tFR_2D Also, with /Ox in MSCV full path is visible even for raw past_last_slash(__FILE__): godbolt.org/z/B1M1nR Commented Jun 6, 2019 at 8:06
  • Also, seems this macro is not valid C++ code, but gcc extension: gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html With --pedantic gcc rejects it. Commented Jun 6, 2019 at 8:13
  • Hm, strange, godbolt failed to do it in compile time for MSVC... I will try to provide some working example. Commented Jun 6, 2019 at 13:13
  • Your godbolt demo shows that the full string ("./example.cpp") is still stored in the executable. The compiler just computes an offset at compile time (mov edi, offset .L.str+2). Commented Jun 6, 2019 at 13:46
  • 1
    In the distant past when I have cared about this, I have passed an alternate filename as '-D__ALTNAME__="1234.cpp"' then put #line __LINE__ __ALTNAME__ immediately after the #includes. Note that this then makes the compiler report errors in 1234.cpp. Commented Jun 12, 2019 at 18:02

3 Answers 3

6

The idea is to create truncated array of characters, but it needs to use only compile time features. Generating data array through variadic template with pack of char forces compiler to generate data without direct relation to passed string literal. This way compiler cannot use input string literal, especially when this string is long.

Godbolt with clang: https://godbolt.org/z/WdKNjB.

Godbolt with msvc: https://godbolt.org/z/auMEIH.

The only problem is with template depth compiler settings.

First we define int variadic template to store sequence of indexes:

template <int... I>
struct Seq {};

Pushing int to Seq:

template <int V, typename T>
struct Push;

template <int V, int... I>
struct Push<V, Seq<I...>>
{
    using type = Seq<V, I...>;
};

Creating sequence:

template <int From, int To>
struct MakeSeqImpl;

template <int To>
struct MakeSeqImpl<To, To>
{
    using type = Seq<To>;
};

template <int From, int To>
using MakeSeq = typename MakeSeqImpl<From, To>::type;

template <int From, int To>
struct MakeSeqImpl : Push<From, MakeSeq<From + 1, To>> {};

Now we can make sequence of compile time ints, meaning that MakeSeq<3,7> == Seq<3,4,5,6,7>. Still we need something to store selected characters in array, but using compile time representation, which is variadic template parameter with characters:

template<char... CHARS>
struct Chars {
    static constexpr const char value[] = {CHARS...};
};
template<char... CHARS>
constexpr const char Chars<CHARS...>::value[];

Next we something to extract selected characters into Chars type:

template<typename WRAPPER, typename IDXS>
struct LiteralToVariadicCharsImpl;

template<typename WRAPPER, int... IDXS>
struct LiteralToVariadicCharsImpl<WRAPPER, Seq<IDXS...> > {
    using type = Chars<WRAPPER::get()[IDXS]...>;
};

template<typename WRAPPER, typename SEQ>
struct LiteralToVariadicChars {
    using type = typename LiteralToVariadicCharsImpl<WRAPPER, SEQ> :: type;
};

WRAPPER is a type that contain our string literal.

Almost done. The missing part is to find last slash. We can use modified version of the code found in the question, but this time it returns offset instead of pointer:

static constexpr int PastLastOffset(int last_offset, int cur, const char * const str)
{
    if (*str == '\0') return last_offset;
    if (*str == '/') return PastLastOffset(cur + 1, cur + 1, str + 1);
    return PastLastOffset(last_offset, cur + 1, str + 1);
}

Last util to get string size:

constexpr int StrLen(const char * str) {
    if (*str == '\0') return 0;
    return StrLen(str + 1) + 1;
}

Combining everything together using define:

#define COMPILE_TIME_PAST_LAST_SLASH(STR)                                   \
    [](){                                                                   \
        struct Wrapper {                                                    \
            constexpr static const char * get() { return STR; }             \
        };                                                                  \
        using Seq = MakeSeq<PastLastOffset(0, 0, Wrapper::get()), StrLen(Wrapper::get())>; \
        return LiteralToVariadicChars<Wrapper, Seq>::type::value; \
    }()

Lambda function is to have nice, value-like feeling when using this macro. It also creates a scope for defining Wrapper structure. Generating this structure with inserted string literal using macro, leads to situation when the string literal is bounded to type.

Honestly I would not use this kind of code in production. It is killing compilers.

Both, in case of security reasons and memory usage, I would recommend using docker with custom, short paths for building.

Sign up to request clarification or add additional context in comments.

2 Comments

Amazing! It doesn't store full filepath in binary, only the filne name, which is actually used. Thanks a lot
@yudjin Previous version did not forced compiler to generate compile-time array. Check current answer (works also with msvc).
6

You can using std::string_view:

constexpr auto filename(std::string_view path)
{ 
    return path.substr(path.find_last_of('/') + 1);
}

Usage:

static_assert(filename("/home/user/src/project/src/file.cpp") == "file.cpp");
static_assert(filename("./file.cpp") == "file.cpp");
static_assert(filename("file.cpp") == "file.cpp");

See it compile (godbolt.org).

For Windows:

constexpr auto filename(std::wstring_view path)
{ 
    return path.substr(path.find_last_of(L'\\') + 1);
}

3 Comments

This doesn't work in C++14, as the question asks. For C++20 there are better solutions.
@jkflying. It works in C++17, which is also OP's tag.
This does not work. The whole path is still stored in the binary, which defeats the purpose of wanting a substring of __FILE__. (see UPD2 comment in the OP)
2

With C++17, you can do the following (https://godbolt.org/z/68PKcsPzs):

#include <cstdio>
#include <array>

namespace details {
template <const char *S, size_t Start = 0, char... C>
struct PastLastSlash {
    constexpr auto operator()() {
        if constexpr (S[Start] == '\0') {
            return std::array{C..., '\0'};
        } else if constexpr (S[Start] == '/') {
            return PastLastSlash<S, Start + 1>()();
        } else {
            return PastLastSlash<S, Start + 1, C..., (S)[Start]>()();
        }
    }
};
}

template <const char *S>
struct PastLastSlash {
    static constexpr auto a = details::PastLastSlash<S>()();
    static constexpr const char * value{a.data()};
};


int main() {
    static constexpr char f[] = __FILE__;
    puts(PastLastSlash<f>::value);
    return 0;
}

With C++14, it's a bit more complicated because of the more limited constexpr (https://godbolt.org/z/bzGec5GMv):

#include <cstdio>
#include <array>

namespace details {
// Generic form: just add the character to the list
template <const char *S, char ch, size_t Start, char... C>
struct PastLastSlash {
    constexpr auto operator()() {
        return PastLastSlash<S, S[Start], Start + 1, C..., ch>()();
    }
};

// Found a '/', reset the character list
template <const char *S, size_t Start, char... C>
struct PastLastSlash<S, '/', Start, C...> {
    constexpr auto operator()() {
        return PastLastSlash<S, S[Start], Start + 1>()();
    }
};

// Found the null-terminator, ends the search
template <const char *S, size_t Start, char... C>
struct PastLastSlash<S, '\0', Start, C...> {
    constexpr auto operator()() {
        return std::array<char, sizeof...(C)+1>{C..., '\0'};
    }
};
}

template <const char *S>
struct PastLastSlash {
    const char * operator()() {
        static auto a = details::PastLastSlash<S, S[0], 0>()();
        return a.data();
    }
};


static constexpr char f[] = __FILE__;
int main() {
    puts(PastLastSlash<f>{}());
    return 0;
}

With C++20 (https://godbolt.org/z/PKEhxdrEz):
(Note: it's not clear if it's safe in MSVC. Godbolt shows that the full string is encoded in the name of the template, but when I search the binary, I do not find it)

#include <cstdio>
#include <array>
#include <string>

namespace details {
template<size_t N>
struct PastLastSlash {
    constexpr PastLastSlash(const char (&s)[N]) {
        std::string str{s};
        size_t pos = str.rfind('/');
        if (pos == std::string::npos) {
            pos = 0;
        } else {
            ++pos;
        }
        size_t len = str.length() - pos + 1;
        std::copy(s+pos, s+pos+len, value.data());      
    }
    constexpr size_t len() const {
        std::string str{value.data()};
        return str.length()+1;
    }
    std::array<char, N> value{};
};
}

template <details::PastLastSlash S>
struct PastLastSlash {
    static constexpr std::array<char, S.len()> copy() {
        std::array<char, S.len()> arr;
        std::copy(S.value.data(), S.value.data()+S.len(), arr.data());      
        return arr;
    }
    static constexpr std::array<char, S.len()> arr{copy()};
    static constexpr const char *value = arr.data();
};


int main() {
    puts(PastLastSlash<__FILE__>::value);
    return 0;
}

Comments

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.