3

Suppose I have a type F. I know that F is empty, but F has no default constructor, so I can't use F() to construct it. Is there a way to obtain a valid object of type F anyway? I seem to recall a mention that there was such a way with arcane usage of unions. Ideally, it would be constexpr friendly.


This can be useful because captureless lambdas only gained a default constructor in C++20. In C++17, if I want to "pass a lambda to a template" and call that lambda without having an instance of it, I need to be able to reconstruct it from the type.

auto const f = [](int x) { return x; };
using F = decltype(f);

static_assert(std::is_empty_v<F>);
static_assert(!std::is_default_constructible_v<F>);

magically-construct-an-F(42);
Justin
  • 24,288
  • 12
  • 92
  • 142
  • 2
    "*In C++17, if I want to "pass a lambda to a template"*" Stop wanting to do that. Just create a `struct` with an `operator()` overload. It's not that much more typing: `struct F {auto operator()(int x) {return x;}};` – Nicol Bolas Jul 12 '19 at 13:38
  • What templates want to default construct function objects? Are you trying to exclude actual functions? – Caleth Jul 12 '19 at 14:43
  • @Caleth: The comparison functor given to `std::set/map`, for example. It must be default-constructible, so that it can be used to compare elements. – Nicol Bolas Jul 12 '19 at 14:54
  • @Caleth In short, for my particular use case, this allows you to lift member functions of the empty object to be static member functions, saving you a pointer argument. In my particular use case, the compiler cannot remove that pointer. But also, for my particular use case, I found a possible workaround. – Justin Jul 12 '19 at 15:44

2 Answers2

8

For your own types, you could copy- or move-construct an object from itself: F f = f. This does not lead to UB by itself, see CWG363.

Language Lawyer
  • 3,378
  • 1
  • 12
  • 29
  • 1
    "*You can copy- or move-construct an object from itself.*" OK, so... how do you get "itself"? – Nicol Bolas Jul 12 '19 at 19:03
  • 2
    @NicolBolas `MyClass foo(foo);` I assume? – HolyBlackCat Jul 12 '19 at 19:31
  • What if you also guarantee `std::is_trivially_copyable_v`? In that case, the copy constructor should be a no-op, so this could be guaranteed to work. – Justin Jul 12 '19 at 20:27
  • Assuming that this is legit (I want to know why this doesn't count as accessing `f` which, because its lifetime hasn't started yet, constitutes UB), this breaks so much of C++. You can't keep people from creating instances of an object outside of your control by deleting all non-public, non-copy/move constructors. So factory-created types and private-access key types are all completely broken by this. – Nicol Bolas Jul 13 '19 at 00:00
  • @NicolBolas "this breaks so much of C++" only if the copy constructor doesn't read from the argument. If the copy constructor does read from the argument, that's definitely UB, because it's accessing `f`, but if it doesn't, it's defined behavior. – Justin Jul 13 '19 at 00:38
  • 1
    @Justin: I mean it *logically* breaks C++, as I explained in the subsequent sentences. Things that the language clearly thinks you're supposed to be able to do and/or prevent don't actually prevent those things. It'd be like if `x.y` followed the rules of public/private, but `x->y` didn't and let you access everything. `private` would still technically exist and work, but it's trivial to make it meaningless. So too here: the ability to control who gets to construct your types is *supposed to exist* in C++. – Nicol Bolas Jul 13 '19 at 00:57
  • 1
    @NicolBolas _I want to know why this doesn't count as accessing f_ [By the definition of access?](https://timsong-cpp.github.io/cppwp/n4659/defns.access) – Language Lawyer Jul 13 '19 at 09:26
  • @NicolBolas `private` is well enforced but `protected` isn't as you can get a pointer to member from any derived class and access the member for an object of any type (base or derived). So by your metric the rule of protected access is broken. – curiousguy Jul 17 '19 at 22:19
  • @curiousguy: `protected` is *supposed* to be accessible from derived classes. That's not a loophole; that's by design. – Nicol Bolas Jul 17 '19 at 22:51
  • @NicolBolas Only static members should be freely accessible from *any* derived class. – curiousguy Jul 18 '19 at 02:23
  • @curiousguy: What is the basis for that claim? The definition of `protected` is that the protected member is accessible from derived classes; that is the design of the feature. You can disagree with the design, but that's different from what's happening here. Here, we have the design of `private` being violated by this hack method to construct an object that has no accessible constructor *from anywhere*. This is three completely separate rules that, when combined in this way, breaks access controls. That's a very different kind of thing from saying that the system is *designed* to be that way. – Nicol Bolas Jul 18 '19 at 02:27
  • @NicolBolas See Stroustrup about the design (I think in D&E); `protected` is in principle restricted to each specific derived class; inherited members of *another* derived class are not supposed to be accessible. (This obviously doesn't apply to static members.) – curiousguy Jul 18 '19 at 02:30
  • @curiousguy: Considering that Java and C# both seem perfectly fine with taking C++'s rules as what they are rather than what you or Stroustrup wanted them to be, I have a hard time buying the idea that `protected` ought to be that way. Equally importantly, that is *completely irrelevant* to this discussion, as this problem is one caused by a confluence of entirely disparate rules (the rule allowing the use of a variable name before the object exists, and the rule deciding what "access" means for a default constructor). Please stop adding irrelevant stuff to other peoples' discussions. – Nicol Bolas Jul 18 '19 at 02:35
  • @NicolBolas The rules are what Stroustrup wanted to be, actually. Also **you** brought up access control. – curiousguy Jul 18 '19 at 04:24
  • @curiousguy private can also be loopholed past, [even quite easily as of C++20](https://dfrib.github.io/a-foliage-of-folly/). I wouldn’t blaim the language for this though, as it is just that, loopholes (that any sane code reviewer would block unless there’s a _really_ good reason to (ab)use it. – dfrib Jun 15 '21 at 21:15
4

but F has no default constructor

If that's the case, then the user either explicitly deleted it or it was implicitly deleted because the user provided some other constructor. In either case, the type is not Trivial.

If an object is non-Trivial, then to create an object of that type without copying/moving from an existing instance, you must explicitly call some sort of constructor. There's no getting around that.

Even the common initial sequence rules of a union don't allow you to create the other object. It only permits access to the non-static data members of the other object. Since your object is empty, this is of no value to you.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • "Even the common initial sequence rules of a union don't allow you to create the other object" Maybe compiler bug reports are in order, then. Every compiler I tested accepts this code which does so in `constexpr`: https://godbolt.org/z/RSEfax – Justin Jul 12 '19 at 13:42
  • @Justin: Then those compilers are buggy. To allow such a thing would completely break the C++ object model. – Nicol Bolas Jul 12 '19 at 13:47
  • The fact that gcc, clang, MSVC, icc, and every other compiler I tested allows this at `constexpr` (implying that they believe it's defined behavior) indicates that there may be something more going on here. It seems unlikely that every compiler has this same bug. Or it's possible that every compiler allows this as implementation-defined – Justin Jul 12 '19 at 13:51
  • 2
    @Justin: [This is intro.object/1](https://timsong-cpp.github.io/cppwp/intro.object#1). It specifies the ways in which an object can come into existence. The rules for implicitly creating a union member do not include returning the named member from a function. Therefore, that's not one of them. So until you can show where the standard explicitly says that this isn't UB, that this code somehow causes the object to appear, then all of those compilers are broken in that way. Also, if you can find such a thing, file it as a defect report, because you're *not supposed to be able to do that*. – Nicol Bolas Jul 12 '19 at 13:54
  • @Justin: What are you passing to `f2` in that example, and how/why? – Davis Herring Jul 12 '19 at 15:06
  • I found the place where I saw some arcane way to do this. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0624r2.pdf links to https://github.com/ldionne/dyno/blob/03eaeded898225660787f03655edb89642a72e7c/include/dyno/detail/empty_object.hpp#L13 . On further reflection, I think that this is UB. The pointer was reconstructed properly, but the object is out-of-lifetime and [\[basic.life\]](https://timsong-cpp.github.io/cppwp/basic.life#7) says there's UB if "the glvalue is used to access the object", which copying it is an access, even if it is trivially copyable. – Justin Jul 12 '19 at 19:08
  • @Justin I know this is quite old, but the godbolt you provided [does not compile on recent versions of `gcc`](https://godbolt.org/z/91fK5W6df) (none starting 10.1), and [it's not accepted by `MSVC` either](https://godbolt.org/z/9WbcMs14G) (any version, regardless of the architecture, starting 19.14). `clang` accepts it for some reason. – Erel Dec 18 '22 at 18:55