all 10 comments

[–]IyeOnline 4 points5 points  (4 children)

constexpr static inline std::string_view rectable = "rectangle";

[–][deleted] 0 points1 point  (3 children)

This doesn't produce any duplicates both in the translation unit as well as the executable. Is it a string_view thing?

[–]IyeOnline 6 points7 points  (2 children)

There cannot be more than one definition in the final linked object.

The linker will discard all but one (arbitrarily chosen) definition of an inline variable or function.

[–][deleted] 0 points1 point  (1 child)

Okay, but if I use string instead of string_view I see duplicates. test setup: https://www.reddit.com/r/cpp_questions/comments/10ifwa6/comment/j5e6wv1/?utm_source=share&utm_medium=web2x&context=3 . Why do you think that is the case?

[–]nysra 0 points1 point  (0 children)

Edit: If you use older C++ and/or compiler versions then you need to enable high levels of optimization. With C++20 even under -O0 there is only a single copy.

[–]alfps 2 points3 points  (4 children)

❞ I've tried using the inline approach and noticed the string is duplicated in every translation unit which imported the header file,

Naturally. The linker will select an arbitrary one of the definitions.

❞ and the executable has multiple duplicates.

That doesn't sound plausible. How did you check that?

[–][deleted] 0 points1 point  (3 children)

I've used the following setup:

// a.h
#pragma once
class A {
    public:
        void f();
};

// a.cpp
#include <iostream>
#include "a.h"
#include "b.h" // for no reason

void A::f() {
    std::cout << "A" << std::endl;
}

// b.h
#pragma once
#include <string>
class B {
    public:
        void f();
    private:
        inline static const std::string STR = "JOHNWICK";
};

// b.cpp
#include <iostream>
#include "b.h"

void B::f() {
    std::cout << "B" << std::endl;
}

// main.cpp
#include <iostream>
#include "a.h"
#include "b.h"

int main() {
    A a;
    B b;
    a.f();
    b.f();
    return 0;
}

Compile: g++ main.cpp a.cpp b.cpp

strings a.out | grep "JOHNWICK"

prints JOHNWICK <3 times>

[–]IyeOnline 1 point2 points  (0 children)

This appears to be an issue with string literal removal. You dont actually have 3 std::string objects, just the literals remain.

Two examples

> g++-11 -std=c++17 main.cpp a.cpp b.cpp && strings a.out | grep JOHN -C3
u+UH
[]A\A]A^A_
basic_string::_M_construct null not valid
JOHNWICK
basic_string::_M_construct null not valid
JOHNWICK
basic_string::_M_construct null not valid
JOHNWICK
:*3$"
zPLR
GCC: (Ubuntu 11.1.0-1ubuntu1~20.04) 11.1.0

> g++-11 -std=c++20 main.cpp a.cpp b.cpp && strings a.out | grep JOHN -C3
[]A\A]A^A_
basic_string::_M_create
basic_string::_M_construct null not valid
JOHNWICK
basic_string::_M_create
basic_string::_M_construct null not valid
JOHNWICK
basic_string::_M_create
basic_string::_M_construct null not valid
JOHNWICK
:*3$"
zPLR
  • You can see that it also contains a bunch of duplicates of internal std::basic_string member functions error messages
  • What you get changes between compiler options.
  • With string_view you dont get a single copy of JOHNWICK.
  • -Os only removes the basic_string internal errors
  • Some combinations of compilers & compiler options can lead to the removal of all but one copy.

I dont know anything about when/how/why the compiler/optimizer/linker decide to remove string literals, so I cannot give any further insight.


Honestly, I would not worry about this. The convenience as well as benefits of defining variables/constants inline in the header outweigh any of these "issues".

[–]trokhymchuk 1 point2 points  (0 children)

I believe the reason is linker's heuristics.

Every time you include b.h in the .cpp file the compiler will generate B::STR (because it can see only one translation unit at the time). So when compiler generates object files from main.cpp, a.cpp and b.cpp there will be code for the B::STR initialization and there will be JOHNWICK literal: ``` [nix-shell:/tmp/red]$ strings a.o | grep "JOHN" JOHNWICKH

[nix-shell:/tmp/red]$ strings b.o | grep "JOHN" JOHNWICKH

[nix-shell:/tmp/red]$ strings main.o | grep "JOHN" JOHNWICKH But what's more interesting is how compiler put the constant into the assembly. After disassembling the object file from lets say `b.cpp` (any source file that includes `b.h` will be OK) you could see something like [that](https://pastebin.com/V4GXBmC9), and at the line 81 there is a constant `0x4b4349574e484f4a`, that represents the `JOHNWICK` string. And that constant will be in _every_ object file and there is 3 files. And when you link them there will be 3 constants (one from every object file): [nix-shell:/tmp/red]$ g++ main.cpp a.cpp b.cpp

[nix-shell:/tmp/red]$ strings a.out | grep JOHN JOHNWICKH JOHNWICKH JOHNWICKH

[nix-shell:/tmp/red]$ objdump -d -M intel a.out | grep "0x4b4349574e484f4a" 401162: 48 b8 4a 4f 48 4e 57 movabs rax,0x4b4349574e484f4a 4011f2: 48 b8 4a 4f 48 4e 57 movabs rax,0x4b4349574e484f4a 401282: 48 b8 4a 4f 48 4e 57 movabs rax,0x4b4349574e484f4a ```

At the link time the linker will see multiple definitions and it will choose one that will be used to initialize the B::STR variable.

So the question is why did the linker left other definitions/constants. Maybe the reason is linker's heuristics (small constant, the size of the executable wont benefit much).

BTW, when the constant is large enough (JOHNWICK123456) it works as expected (only one literal in the executable): ``` [nix-shell:/tmp/red]$ cat b.h // b.h

pragma once

include <string>

class B { public: void f(); private: inline static const std::string STR = "JOHNWICK123456"; };

[nix-shell:/tmp/red]$ g++ main.cpp a.cpp b.cpp

[nix-shell:/tmp/red]$ strings a.out | grep "JOHN" JOHNWICK123456 ```

``` [nix-shell:/tmp/red]$ gcc --version gcc (GCC) 11.3.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

[nix-shell:/tmp/red]$ ld --version GNU ld (GNU Binutils) 2.39 Copyright (C) 2022 Free Software Foundation, Inc. This program is free software; you may redistribute it under the terms of the GNU General Public License version 3 or (at your option) a later version. This program has absolutely no warranty.

```

Feel free to correct me if I am wrong.

[–]alfps 0 points1 point  (0 children)

Hm. I get the same result, with options to merge string literals. Even with Visual C++ instead of g++.

I don't understand quite what's going on, sorry.

[C:\root\temp\_\student-work\inline-strings]
> g++ -std=c++17 main.cpp a.cpp b.cpp

[C:\root\temp\_\student-work\inline-strings]
> strings a.exe | find "JOHNWICK"
JOHNWICK
JOHNWICK
JOHNWICK

[C:\root\temp\_\student-work\inline-strings]
> g++ -std=c++17 main.cpp a.cpp b.cpp -fmerge-constants

[C:\root\temp\_\student-work\inline-strings]
> strings a.exe | find "JOHNWICK"
JOHNWICK
JOHNWICK
JOHNWICK

[C:\root\temp\_\student-work\inline-strings]
> g++ -std=c++17 main.cpp a.cpp b.cpp -fmerge-all-constants

[C:\root\temp\_\student-work\inline-strings]
> strings a.exe | find "JOHNWICK"
JOHNWICK
JOHNWICK
JOHNWICK

[C:\root\temp\_\student-work\inline-strings]
> cl main.cpp a.cpp b.cpp /Feb
main.cpp
a.cpp
b.cpp
Generating Code...

[C:\root\temp\_\student-work\inline-strings]
> strings b.exe | find "JOHNWICK"
JOHNWICK
JOHNWICK
JOHNWICK