35

Using both gcc with -std=c11 and g++ with -std=c++14.

E.g. for a file named src/dir/Hello.cxx it should expand to something like e.g.:

const char basename[] = "Hello";

or

const char basename[] = getStaticBasename(__FILE__);

as where getStaticBasename() is a macro (for C sources) or constexpr function (for C++ sources) which results to "Hello".

I have to avoid splitting the string from __FILE__ at runtime, because the path and suffix must not be compiled into the executable in any way.

The solution must be without dependencies to huge libraries such as boost.

As I have no makefiles, solutions like this cannot be used in my case.

Did one have a solution for that?

Edit 2015-07-02:

  • I have no influence on how the compiler and linker was invoked (sometimes via makefile, sometimes from command line, or some IDE (Eclipse CDT managed make, Crossworks, Xcode et cetera. So the solution needs to be in code only.
  • My use case is to provide some kind of "generic region identifier" for a small footprint logging solution. The application code (which uses my logger) should only #include <Joe/Logger.h> and within the later calls to e.g. LOG_DEBUG(...) I'll implicitely take use of the automatically generated "generic region identifier".
  • My current solution is that the application code have to declare a JOE_LOG_FILE_REGION(Hello); (after #include <Joe/Logger.h>) before it could place LOG_DEBUG(...) in its code.
Community
  • 1
  • 1
Joe
  • 3,090
  • 6
  • 37
  • 55
  • 1
    The C++ one is fairly easy. I can't think of how it would be done in C. – Dark Falcon Jun 25 '15 at 12:34
  • 2
    I think it's best to split this question into two, as c++ can look very differently due to possibility of using `constexpr`s. – luk32 Jun 25 '15 at 12:46
  • Question is not duplicated because you want to do it at compile time but one answer there is what you need: [`__FILE__` macro shows full path](http://stackoverflow.com/a/16658858/1207195) – Adriano Repetti Jun 25 '15 at 12:51
  • 2
    @DarkFalcon Why don't you add your C++ solution as an answer, or at least give some hints? Yes it won't answer the C part, but it's still better than nothing. – Some programmer dude Jun 25 '15 at 12:57
  • 1
    @JoachimPileborg: I'm afraid I lack the time (and maybe the smarts) to put that together, but here is an implementation by someone else which provides the harder bits (the substr): https://github.com/dscharrer/void/blob/master/c%2B%2B/constexpr_string.cpp – Dark Falcon Jun 25 '15 at 13:34
  • The `makefile` solution can be adapted to the command line. Just have some script to build your thing, or even use some script instead of `g++` – Basile Starynkevitch Jul 01 '15 at 21:43
  • I need a solution that is independent on how the compiler is invoked. – Joe Jul 02 '15 at 14:03
  • Yep, I think I'll split it into two questions for C and C++ – Joe Jul 02 '15 at 14:32
  • Maybe duplicate of https://stackoverflow.com/q/237542/2506522 – betontalpfa Jul 27 '20 at 12:20

6 Answers6

39

1. gcc builtin function can get the file name of a full path at compile time.

#define __FILENAME__ (__builtin_strrchr(__FILE__, '/') ? __builtin_strrchr(__FILE__, '/') + 1 : __FILE__)

or

#define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)

2. c++11 constexpr also can do this at compile time.

c++11 constexpr function can only use a return-statement.

example:

#include <stdio.h>

constexpr const char* str_end(const char *str) {
    return *str ? str_end(str + 1) : str;
}

constexpr bool str_slant(const char *str) {
    return *str == '/' ? true : (*str ? str_slant(str + 1) : false);
}

constexpr const char* r_slant(const char* str) {
    return *str == '/' ? (str + 1) : r_slant(str - 1);
}
constexpr const char* file_name(const char* str) {
    return str_slant(str) ? r_slant(str_end(str)) : str;
}

int main() {
    constexpr const char *const_file = file_name(__FILE__);
    puts(const_file);
    return 0;
}

source file name is foo/foo1/foo2/foo3/foo4.cpp

use g++ -o foo.exe foo/foo1/foo2/foo3/foo4.cpp -std=c++11 --save-temps to compile this file.

you can see this.

.file   "foo4.cpp"
        .section        .rodata
.LC0:
        .string "foo/foo1/foo2/foo3/foo4.cpp"
        .text
        .globl  main
        .type   main, @function
main:
.LFB4:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        movq    $.LC0+19, -8(%rbp) 
        movl    $.LC0+19, %edi
        call    puts
        movl    $0, %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE4:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
        .section        .note.GNU-stack,"",@progbits

movl $.LC0+19, %edi .LC0 + 19 is the address of file name string without path and suffix

3. c++14 constexpr function can do this in a simple way

#include <iostream>

constexpr const char* file_name(const char* path) {
    const char* file = path;
    while (*path) {
        if (*path++ == '/') {
            file = path;
        }
    }
    return file;
}

int main() {
    constexpr const char* file = file_name(__FILE__);
    std::cout << file << std::endl;
    return 0;
}

c++14 constexpr function can use loop and local variable.

the file_name function will replace with a address of const char * at compiler time. ~

pexeer
  • 685
  • 5
  • 8
  • At least with gcc 6.3.0, the compiler does not allow `__builtin_strrchr` to be used in the initializer of a static variable, because "initializer element is not constant". – mhsmith Jun 08 '17 at 15:45
  • 2
    I've tried your example with plain *strrchr* and *strstr* and in both cases optimization took place, so it seems it is not necessary to use __builtin_strrchr. Compiler: "GCC: (Debian 6.3.0-18+deb9u1) 6.3.0 20170516". – Andrew Selivanov Mar 28 '18 at 09:46
  • cute, they're extending the preprocessor but at the same time they don't give it full eval power of switching to php, so it can't delete files/google things/fetch/send network requests from preprocessor – Dmytro Jun 15 '18 at 18:07
  • i tried to use your `#define __FILENAME__` with `constexpr const char* basename=__FILENAME__` but my g++ 4.8.5 complains about a function not being `constexpr`. so it seems that this solution depends on having compiler optimizations enabled. – Trevor Boyd Smith May 14 '19 at 12:30
  • 1
    I prefer your c++ only solution because it is always compile time (and you don't have to worry about optimizer being turned off and then your #define is runtime). – Trevor Boyd Smith May 14 '19 at 12:31
  • I wonder if compile times will be affected in projects that use the latter metafunction heavily. – Emile Cormier Jan 27 '20 at 19:40
  • 6
    These solutions leave the suffix so they does not satisfy the question. Or am I missing something? – shargors Feb 20 '20 at 16:25
12

extract the base filename at compile time with no preprocessor tricks and no external scripts? c++14? no problem sir.

#include <iostream>
#include <string>

using namespace std;

namespace detail {
    constexpr bool is_path_sep(char c) {
        return c == '/' || c == '\\';
    }

    constexpr const char* strip_path(const char* path)
    {
        auto lastname = path;
        for (auto p = path ; *p ; ++p) {
            if (is_path_sep(*p) && *(p+1)) lastname = p+1;
        }
        return lastname;
    }

    struct basename_impl
    {
        constexpr basename_impl(const char* begin, const char* end)
        : _begin(begin), _end(end)
        {}

        void write(std::ostream& os) const {
            os.write(_begin, _end - _begin);
        }

        std::string as_string() const {
            return std::string(_begin, _end);
        }

        const char* const _begin;
        const char* const _end;
    };

    inline std::ostream& operator<<(std::ostream& os, const basename_impl& bi) {
        bi.write(os);
        return os;
    }

    inline std::string to_string(const basename_impl& bi) {
        return bi.as_string();
    }

    constexpr const char* last_dot_of(const char* p) {
        const char* last_dot = nullptr;
        for ( ; *p ; ++p) {
            if (*p == '.')
                last_dot = p;
        }
        return last_dot ? last_dot : p;
    }
}

// the filename with extension but no path
constexpr auto filename = detail::strip_path(__FILE__);
constexpr auto basename = detail::basename_impl(filename, detail::last_dot_of(filename));

auto main() -> int
{
    cout << filename << endl;
    cout << basename << endl;

    cout << to_string(basename) << endl;

    return 0;
}
dyp
  • 38,334
  • 13
  • 112
  • 177
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • plast points to the last path separator (or beginning of string if there is none) so you want to return one past the separator to return the filename. However, you also don't want to return an empty string if the path happens to end with a slash (it's a corner case I know, but still...) – Richard Hodges Jul 02 '15 at 18:58
  • 2
    cute, this constexpr thing really wants to become it's own programming language like PHP. – Dmytro Jun 15 '18 at 18:08
  • @Dmitry re "constexpr .. it's own programming language": myself and many articles I've read do consider `constexpr` it's own language. in addition there is the c++ templates... they are another language. preprocessor is another language. – Trevor Boyd Smith May 14 '19 at 12:35
  • RichardHodges your implementation is about 2 or 3 times more lines of code than @pexeer's [solution](https://stackoverflow.com/a/38237385/52074) and uses two for loops, uses struct, uses std::string, uses operator overloading, uses std::ostream. what features/added-benefit does your implementation do compared to @pexeer's? – Trevor Boyd Smith May 14 '19 at 12:41
  • @TrevorBoydSmith as far as I can see, I'm offering a little more utility in terms of providing ostream and to_stream overloads, and handling both windows and unix paths. However, peexer's solution was written a year after mine so he may have had more time to digest the question. – Richard Hodges May 14 '19 at 15:51
  • 4
    @RichardHodges: More than that, yours is the only C++ solution that actually removes the suffix. – Fake Code Monkey Rashid Aug 28 '20 at 08:55
  • `std::string_view` in C++17 can greatly reduce the line of code (`basename_impl`). – guan boshen Jan 24 '23 at 02:29
9

If you run gcc from the folder where the source file is located, you will get a different __FILE__ than if you pass an absolute path (i.e. handed to gcc through an IDE).

  • gcc test.c -otest.exe gives me __FILE__ as test.c.
  • gcc c:\tmp\test.c -otest.exe gives me __FILE__ as c:\tmp\test.c.

Perhaps calling gcc from the path where the source is located is sufficient as work-around?


EDIT

Here is a "dirty" but safe hack which removes the file extension in compile-time. Not really something I'd recommend, but it was fun to write :) So take it for what it is worth. It only works in C.

#include <stdio.h>

#define EXT_LENGTH (sizeof(".c") - 1) // -1 null term

typedef union
{
  char filename_no_nul [sizeof(__FILE__)-EXT_LENGTH-1]; // -1 null term
  char filename_nul    [sizeof(__FILE__)-EXT_LENGTH];
} remove_ext_t;

int main (void)
{
  const remove_ext_t file = { __FILE__ };

  puts(file.filename_nul);

  return 0;
}

The union allocates one member which is large enough to hold the full path minus extension and null terminator. And it allocates one member which is large enough to hold the full path minus extension, though with a null terminator.

The member which is too small to hold the full __FILE__ is initialized with as much of __FILE__ as can fit. This is ok in C but not allowed in C++. If __FILE__ contains test.c, the union member will now be initialized to contain test with no null terminator.

There will however still be trailing zeroes after that string, because this hack abuses the fact that the other union member has been initialized according to the rules of "aggregate/union" initialization. This rule forces any remaining items in the "aggregate" to be initialized as if they had static storage duration, i.e to zero. Which happens to be the value of the null terminator.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • This is really smart, what can I say? ;). – Iharob Al Asimi Jun 25 '15 at 13:55
  • Yes, quite hackish, and I know some who would shoot me with a rope using such that. However, you could pack that into a header using 'static` and include in every file. – too honest for this site Jun 25 '15 at 13:56
  • There is text in the standard saying “If there are fewer initializers in a brace-enclosed list than there are elements or members of an aggregate, or fewer characters in a string literal used to initialize an array of known size than there are elements in the array, the remainder of the aggregate shall be initialized implicitly the same as objects that have static storage duration.” But this cannot literally be true, or `union { int a, b; } = { 3 }` would leave 0 in the union. It must be interpreted in light of… – Eric Postpischil Jan 11 '18 at 10:31
  • … “When no designations are present, subobjects of the current object are initialized in order according to the type of the current object: array elements in increasing subscript order, structure members in declaration order, and the first named member of a union.” – Eric Postpischil Jan 11 '18 at 10:32
4

It turns out to be very simple, you just need the #line preprocessor directive, example

#line 0 "Hello"

at the top of the file, this as is, if all you want is to hide the file name completely then

#line 0 ""

would work.

If you don't want to use Makefiles, you can use this

file=cfile;
content=$(sed -e "1s/^/#line 0 \"$file\"\n/" example/${file}.c);
echo $content | gcc -xc -O3 -o ${file} -

The -xc gcc flag above means (from gcc's documentation):

-x language:

Specify explicitly the language for the following input files (rather than letting the compiler choose a default based on the file name suffix). This option applies to all following input files until the next -x option. Possible values for language are:

          c  c-header  cpp-output
          c++  c++-header  c++-cpp-output
          objective-c  objective-c-header  objective-c-cpp-output
          objective-c++ objective-c++-header objective-c++-cpp-output
          assembler  assembler-with-cpp
          ada
          f77  f77-cpp-input f95  f95-cpp-input
          go
          java

If you don't have any sort of script that helps you building the source then there is no way to do it I think.

Also, you can see from the above quote of the gcc documentation, that you can save the files without any extension at all, and then combine @Lundin's original solution with this and use

gcc -xc -o file filename_without_extension

in this case __FILE__ would expand to "filename_without_extension", and you would achieve what you want, although you need to compile the file in the same directory where it lives, because otherwise it will contain the path to the file.

Community
  • 1
  • 1
Iharob Al Asimi
  • 52,653
  • 6
  • 59
  • 97
  • How do you put the string `"hello"` in the source file (i.e. in the `#line` directive)? The file name can be anything, not just `hello.cpp`. – P.P Jun 25 '15 at 12:58
  • 1
    That becomes a nightmare when renaming files. – too honest for this site Jun 25 '15 at 13:16
  • @Olaf which is why using a Makefile is the best solution. – Iharob Al Asimi Jun 25 '15 at 13:18
  • 3
    This doesn't look very helpful. If you have to place the string of interest into the source, anyhow, you can just as well use it directly to initialize the variable. – Jens Gustedt Jun 25 '15 at 13:36
  • That would hardly help as such inside the code. (but would allow to pass a macro) – too honest for this site Jun 25 '15 at 13:40
  • @Olaf you are right, I don't think there is a solution that meets **all** the requirements. The good thing about this solution is that it doesn't require changing the `__FILE__` macro if it was already used. – Iharob Al Asimi Jun 25 '15 at 13:41
  • @JensGustedt You can of course downvote if you like. – Iharob Al Asimi Jun 25 '15 at 13:51
  • If have required similar, but I pass it from SCons by `-DFILE_BASE=...`. The name is processed in Python/SCons that way. – too honest for this site Jun 25 '15 at 13:51
  • Why do you use `scons`? it's so complicated to use python tools, it's version 3 or 2 and all that cr*p... But yes, you are right, that would be a perfect solution, if the OP wanted to use build scripts of any kind. I don't like `scons` because my linux distro has a package manager rolled by my self, so if I need some new package I must compile it from source, which is a nightmare when the build tools are `scons` or `waf`. – Iharob Al Asimi Jun 25 '15 at 13:53
  • What I meant is that you could just do `gcc -O3 -o ${file} -DFILE_BASE="\"${file}\"" "${file}.c"` directly without passing your source through `sed` and complicated stuff like that. – Jens Gustedt Jun 25 '15 at 14:08
1

The most voted solution does not rely answer the OP, because the complete file path is stored in the binary and only a pointer to the last part of the path is computed (from the last '/' character) and used.

See assembly output of the proposed solution in @pexeer answer:

.LC0:
        .string "/app/example.cpp"
main:
        push    rax
        mov     esi, OFFSET FLAT:.LC0+5
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
        xor     eax, eax
        pop     rdx
        ret
_GLOBAL__sub_I_main:
        push    rax
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        pop     rcx
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        jmp     __cxa_atexit

To avoid storing the complete file path, you'll need something like this:

#include <iostream>
#include <utility>

constexpr const char* file_name(const char* path) {
    const char* file = path;
    while (*path) {
        if (*path++ == '/') {
            file = path;
        }
    }
    return file;
}

constexpr size_t file_length(const char * path) {
    size_t i = 0;
    const char * file = file_name(path);
    while (*file) { i ++; file++; }
    return i;
}

template<std::size_t... I>
const char * print_impl(std::index_sequence<I...>) {
    static const char file[file_length(__FILE__)+1] = { file_name(__FILE__)[I]...};
    return file;
}

inline const char* print_file() {
    return print_impl(std::make_index_sequence<file_length(__FILE__) + 1>());
}


int main() {
    std::cout<<print_file()<<std::endl;
    return 0;
}

and you'll get this assembly output (where the complete file path isn't stored):

main:
        push    rax
        mov     esi, OFFSET FLAT:print_impl<0ul, 1ul, 2ul, 3ul, 4ul, 5ul, 6ul, 7ul, 8ul, 9ul, 10ul, 11ul>(std::integer_sequence<unsigned long, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul, 6ul, 7ul, 8ul, 9ul, 10ul, 11ul>)::file
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
        xor     eax, eax
        pop     rdx
        ret
_GLOBAL__sub_I_main:
        push    rax
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        pop     rcx
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        jmp     __cxa_atexit
print_impl<0ul, 1ul, 2ul, 3ul, 4ul, 5ul, 6ul, 7ul, 8ul, 9ul, 10ul, 11ul>(std::integer_sequence<unsigned long, 0ul, 1ul, 2ul, 3ul, 4ul, 5ul, 6ul, 7ul, 8ul, 9ul, 10ul, 11ul>)::file:
        .string "example.cpp"

Example here

The basic idea here is to construct a statically initialized char array containing only the string you want (and not a pointer to a static char array containing the full file path). Deducing the file length is trivial but required since we can't call strlen in a constexpr function.

Then the trick is to use a integer sequence as indices in the file's pointed array (like natural declaration: const char f[] = {"str"[0], "str"[1], ...}). The integer sequence can be used in variadic template instantiation, so it must be called in such context.

GCC leaks the print_impl function as a symbol (so it's likely larger that the file's full path), but it can be stripped later on at linker step (or with strip --strip-all /path/to/binary)

xryl669
  • 3,376
  • 24
  • 47
0

Unfortunately it seems everyone is busy removing the unwanted part of the path via various magical ways in code (--> most of them not working).

In my opinion the correct way is to tell the compiler to change/remove the path from the macro avoiding all need for tinkering. With gcc the parameter is called fmacro-prefix-map. You would use it like this:

-fmacro-prefix-map=/path/to/source/=

to change "/path/to/source/main.cpp" to just "main.cpp"

By the way: this also works for std::source_location and of course the full path (unaltered) is not stored in the resulting binary.

Xatian
  • 772
  • 1
  • 8
  • 24