Skip to content

inherited constructor causes std::source_location::current() to point at wrong line #137907

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
snarkmaster opened this issue Apr 30, 2025 · 6 comments
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema"

Comments

@snarkmaster
Copy link

In this code (https://godbolt.org/z/dPhq718e5), Foo foo{} ends up with an incorrect source_location, while Foo foo{Potato{} is fine.

It looks like using Bar::Bar generates a default ctor for Foo, which becomes the evaluation site for source_location::current().

I'm not a standards lawyer, but I didn't find any suggestion that what we're seeing here is the specified behavior. And for default arguments, the the clear intent of the C++ standard is for default arguments to be evaluated at the call site. The current behavior violates at least the spirit of that rule. It also breaks the typical source_location-stamping technique.

#include <iostream>
#include <source_location>

struct Potato {};

struct Bar {
  std::source_location sl_;
  explicit Bar(Potato = {}, std::source_location sl = std::source_location::current())
      : sl_(sl) {}
};

struct Foo : Bar { // `foo.sl_` points here on GCC & clang
  using Bar::Bar;
}; // `foo.sl_` points here on MSVC

int main() {
  Foo foo{}; // Why isn't `sl_` pointing here?
  std::cout << "foo @ line " << foo.sl_.line() << std::endl;
  Foo foo2{Potato{}}; // But this is fine!
  std::cout << "foo2 @ line " << foo2.sl_.line() << std::endl;
  return 0;
}
@EugeneZelenko EugeneZelenko added clang:frontend Language frontend issues, e.g. anything involving "Sema" and removed new issue labels Apr 30, 2025
@llvmbot
Copy link
Member

llvmbot commented Apr 30, 2025

@llvm/issue-subscribers-clang-frontend

Author: None (snarkmaster)

In this code (https://godbolt.org/z/dPhq718e5), `Foo foo{}` ends up with an incorrect `source_location`, while `Foo foo{Potato{}` is fine.

It looks like using Bar::Bar generates a default ctor for Foo, which becomes the evaluation site for source_location::current().

I'm not a standards lawyer, but I didn't find any suggestion that what we're seeing here is the specified behavior. And for default arguments, the the clear intent of the C++ standard is for default arguments to be evaluated at the call site. The current behavior violates at least the spirit of that rule. It also breaks the typical source_location-stamping technique.

#include &lt;iostream&gt;
#include &lt;source_location&gt;

struct Potato {};

struct Bar {
  std::source_location sl_;
  explicit Bar(Potato = {}, std::source_location sl = std::source_location::current())
      : sl_(sl) {}
};

struct Foo : Bar { // `foo.sl_` points here on GCC &amp; clang
  using Bar::Bar;
}; // `foo.sl_` points here on MSVC

int main() {
  Foo foo{}; // Why isn't `sl_` pointing here?
  std::cout &lt;&lt; "foo @ line " &lt;&lt; foo.sl_.line() &lt;&lt; std::endl;
  Foo foo2{Potato{}}; // But this is fine!
  std::cout &lt;&lt; "foo2 @ line " &lt;&lt; foo2.sl_.line() &lt;&lt; std::endl;
  return 0;
}

@snarkmaster snarkmaster changed the title inherited constructor causes source_locationI::current() to point at wrong line inherited constructor causes std::source_location::current() to point at wrong line Apr 30, 2025
@cor3ntin
Copy link
Contributor

cor3ntin commented May 2, 2025

I think clang and gcc are correct here.

struct Bar {
    int sl;
    Bar(int i = 0, int sl = __builtin_LINE()) : sl(sl) {}
};

struct Foo : Bar {
    using Bar::Bar;
};

int main() {
    Foo foo; // #1
    return foo.sl;
}

Per https://eel.is/c++draft/class#inhctor.init-1.sentence-1

When a constructor for type B is invoked to initialize an object of a different type D (that is, when the constructor was inherited ([namespace.udecl])), initialization proceeds as if a defaulted default constructor were used to initialize the D object and each base class subobject from which the constructor was inherited, except that the B subobject is initialized by the inherited constructor if the base class subobject were to be initialized as part of the D object ([class.base.init]).

In #1 we call the default constructor of Foo - which then calls the default constructor of Bar from its point of declaration - which in clang and gcc is the the opening bracket of the class.

If instead you give parameters, the constructor of Bar is explicitly called, then the point of call is where foo is initialized.

@cor3ntin cor3ntin closed this as completed May 5, 2025
@EugeneZelenko EugeneZelenko added the question A question, not bug report. Check out https://llvm.org/docs/GettingInvolved.html instead! label May 5, 2025
@ispeters
Copy link

ispeters commented May 5, 2025

Per https://eel.is/c++draft/class#inhctor.init-1.sentence-1

When a constructor for type B is invoked to initialize an object of a different type D (that is, when the constructor was inherited ([namespace.udecl])), initialization proceeds as if a defaulted default constructor were used to initialize the D object and each base class subobject from which the constructor was inherited, except that the B subobject is initialized by the inherited constructor if the base class subobject were to be initialized as part of the D object ([class.base.init]).

In #1 we call the default constructor of Foo - which then calls the default constructor of Bar from its point of declaration - which in clang and gcc is the the opening bracket of the class.

If instead you give parameters, the constructor of Bar is explicitly called, then the point of call is where foo is initialized.

I think the next few sentences suggest a different interpretation, and this Godbolt shows Clang violates my understanding of those sentences.

Per https://eel.is/c++draft/class#inhctor.init-1.sentence-3

The complete initialization is considered to be a single function call; in particular, unless omitted, the initialization of the inherited constructor's parameters is sequenced before the initialization of any part of the D object.

Taking @snarkmaster's sample code and extending it by giving Foo another base class, like so:

struct Potato {};

struct Bar {
  std::source_location sl_;
  explicit Bar(Potato = {}, std::source_location sl = std::source_location::current())
      : sl_(sl) {}
};

struct Baz {
    int i = 42;
};

struct Foo : Baz, Bar {
  using Bar::Bar;
};

(see this Godbolt for a working example)

The above code generates the following for the Foo default constructor:

Foo::Foo() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 32
        mov     qword ptr [rbp - 8], rdi
        mov     rdi, qword ptr [rbp - 8]
        mov     qword ptr [rbp - 32], rdi
        call    Baz::Baz() [base object constructor]
        mov     rdi, qword ptr [rbp - 32]
        add     rdi, 8
        lea     rax, [rip + .L.constant.5]
        mov     qword ptr [rbp - 24], rax
        mov     rsi, qword ptr [rbp - 24]
        call    Bar::Bar(Potato, std::source_location) [base object constructor]
        add     rsp, 32
        pop     rbp
        ret

I believe that, in this case, the Baz default constructor initializes a "part of the D object" and you can see that the parameters provided to the Bar constructor are initialized after the Baz constructor has returned, which I think is in contravention of this part of sentence 3: "the initialization of the inherited constructor's parameters is sequenced before the initialization of any part of the D object."

I'm a beginner language lawyer so I'm not completely confident in my interpretation. Have I misunderstood?

If I've understood correctly then my further interpretation of [class.inhctor.init] is that, for the initialization of the parameters to the inherited constructor to be definitely initialized before any part of the D object, the parameters must be initialized, in this case, at the call site of Foo::Foo().

@efriedma-quic
Copy link
Collaborator

I think really, there's an issue here with the implicit declaration of default constructors. Consider the following variation:

#include <iostream>
#include <source_location>

struct Potato {};

struct Bar {
  std::source_location sl_;
  explicit Bar(Potato = {}, std::source_location sl = std::source_location::current())
      : sl_(sl) {}
};

struct Foo : Bar {
  Foo(float);
  using Bar::Bar;
};

int main() {
  Foo foo{};
  std::cout << "foo @ line " << foo.sl_.line() << std::endl;
  Foo foo2{Potato{}};
  std::cout << "foo2 @ line " << foo2.sl_.line() << std::endl;
  return 0;
}

This "works" in MSVC; I think this is because it suppresses Foo's default constructor? On clang and gcc, this has no effect. In clang's AST, there's a declaration of an implicit default constructor.

Or another variation:

#include <iostream>
#include <source_location>

struct Potato {};

struct Bar {
  std::source_location sl_;
  explicit Bar(Potato = {}, std::source_location sl = std::source_location::current())
      : sl_(sl) {}
};

struct Foo : Bar {
  template<typename T> Foo(float = 0);
  using Bar::Bar;
};

int main() {
  Foo foo{};
  std::cout << "foo @ line " << foo.sl_.line() << std::endl;
  Foo foo2{Potato{}};
  std::cout << "foo2 @ line " << foo2.sl_.line() << std::endl;
  return 0;
}

This "works" in all three compilers, because it suppresses the default constructor.

This doesn't seem intentional... or at least if it is intentional, I can't find any language supporting it.

@efriedma-quic efriedma-quic reopened this May 6, 2025
@EugeneZelenko EugeneZelenko removed the question A question, not bug report. Check out https://llvm.org/docs/GettingInvolved.html instead! label May 6, 2025
@efriedma-quic
Copy link
Collaborator

Whether we declare an implicit default constructor is observable in other ways; for example:

struct Bar {
  explicit consteval Bar(int = 0) {}
};

int f();
struct Foo1 : Bar {
  using Bar::Bar;
  int x = f();
};
struct Foo2 : Bar {
  Foo2(float);
  using Bar::Bar;
  int x = f();
};
struct Foo3 : Bar {
  template<typename T> Foo3(float = 0);
  using Bar::Bar;
  int x = f();
};

int main() {
  Foo1 foo1{};
  Foo2 foo2{};
  Foo3 foo3{};
  return 0;
}

@efriedma-quic
Copy link
Collaborator

See bc713f6, I guess? CC @zygoloid .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema"
Projects
None yet
Development

No branches or pull requests

6 participants