4

I am working on a library that has a class foo. foo has a non-trivial constructor. When I create an std::array of foo (std::array<foo, 10>), the constructor is called 10 times. I want to implement a separate way of initializing an arrays of foo. Will defining a specialization for std::array<foo, N> result in undefined behavior or any other concerns? If it's OK, what properties does my specialization need to have?

https://en.cppreference.com/w/cpp/language/extending_std says specializations for custom types are allowed unless explicitly disallowed and https://en.cppreference.com/w/cpp/container/array doesn't say anything about it.

TylerH
  • 20,799
  • 66
  • 75
  • 101
Ajay Brahmakshatriya
  • 8,993
  • 3
  • 26
  • 49
  • 3
    Why have a specialization for this instead of making your own class? – Nicol Bolas Dec 20 '22 at 19:36
  • 1
    @NicolBolas I would create a separate class if it is disallowed. Since `foo` is a part of a library, it would be convenient for the end-user if we specialize `std::array`. – Ajay Brahmakshatriya Dec 20 '22 at 19:42
  • 2
    Will this `std::array` do dynamic allocation + placement `new`? If so, it's no longer an aggregate type and doesn't satisfy all requirements for the original template. If not, how will you prevent the default constructor from being called? – Ted Lyngmo Dec 20 '22 at 19:58
  • 1
    @TedLyngmo I was planning to initialize the array type with a initializer list filled with `N` values of a sentinel type. `foo` has a constructor that doesn't call the default constructor logic when this sentinel type is used. The other option was to use a std::byte array and call placement new – Ajay Brahmakshatriya Dec 20 '22 at 20:05
  • I see. Wouldn't it be easier to make the default constructor "cheap" and use another constructor for the expensive creation? You need to keep it an aggregate to satisfy all requirements for the original template in either case. Otherwise, the specialization is not allowed. – Ted Lyngmo Dec 20 '22 at 20:06
  • 1
    Why does `foo` have an expensive default constructor? Remove that, and the problem goes away. Make the constructor accept a sentinel type if it's expensive. – Mooing Duck Dec 20 '22 at 20:06
  • @TedLyngmo unfortunately not, the default constructor is part of the interface of the library. – Ajay Brahmakshatriya Dec 20 '22 at 20:07
  • `libfoo_v2` ? :-) – Ted Lyngmo Dec 20 '22 at 20:08
  • 1
    I meant it is part of the design. This is a multi-stage programming library (https://buildit.so). The type in question (`foo`) here is the `builder::dyn_var`. The default constructor is implemented such that when the user writes `dyn_var x`, it generates `int x` in the second stage code. – Ajay Brahmakshatriya Dec 20 '22 at 20:09
  • 1
    @TedLyngmo an example - https://buildit.so/tryit/?sample=shared&pid=1fc351646ebd4c74553150569299344e – Ajay Brahmakshatriya Dec 20 '22 at 20:12
  • 1
    I found this very interesting and posted a follow-up question: [std class specialization - meeting the standard library requirements for the original std::array template](https://stackoverflow.com/questions/74880400/std-class-specialization-meeting-the-standard-library-requirements-for-the-ori) – Ted Lyngmo Dec 21 '22 at 18:36
  • Alternative: create a `foo` wrapper `bar` that carries a `foo`, has an `operator foo const&() const` and `operator foo&()` and uses the cheap constructor as default constructor. – bitmask Dec 21 '22 at 18:45
  • @bitmask Would that make the `std::array` specialization possible? – Ted Lyngmo Dec 22 '22 at 08:53
  • @TedLyngmo No. My comment was intended as an alternative to a specialisation. I.e. `std::array`. It wasn't clear from OP to me who controls the array object (the library or OP). – bitmask Dec 22 '22 at 12:42
  • @bitmask Aha, ok, sorry for the confusion – Ted Lyngmo Dec 22 '22 at 13:28
  • @bitmask that would need us to change the interface. We really wanted the end user to be able to declare `std::array` – Ajay Brahmakshatriya Dec 22 '22 at 17:53
  • What do you mean by "is it safe"? – TylerH Jan 06 '23 at 16:01
  • @TylerH many other STL references specify "specializing this template in user code leads to undefined behavior". I wanted to check if that is also the case for `std::array`. Or is it defined behavior to specialize if the reference doesn't say anything about the it. – Ajay Brahmakshatriya Jan 07 '23 at 18:19
  • @TylerH I understand C++ doesn't have a well-defined notion of safe, but I think most people understand what that means in this context. – Ajay Brahmakshatriya Jan 07 '23 at 18:20
  • @AjayBrahmakshatriya So you are worried about undefined behavior? – TylerH Jan 08 '23 at 03:07
  • @TylerH The standard and the implementations generally do not allow putting code into the `std` namespace except in a few exceptions. As we do not know, what implementation details are relying on that rule, doing it nevertheless would be unsafe and UB. – Sebastian Jan 08 '23 at 07:32
  • @TylerH yes. But also wanted to check if there are some restrictions that need to be taken care of while implementing if it is defined. I think the question has been answered adequately though – Ajay Brahmakshatriya Jan 08 '23 at 15:13
  • 1
    Right; my concern is that the question is currently needing clarity / opinion-based (regardless of any answers). I want to see if the question can be edited (which I was fairly sure it could be) or if it was not salvageable. But that required input/clarity from you, so thank you for providing it :-) – TylerH Jan 09 '23 at 14:27

2 Answers2

10

Yes, you can specialize std::array<foo, N> for a program-defined foo. However, there are multiple problems with this, only giving two major ones that came to mind here:

  1. Every user of std::array<foo, N> must include the partial specialization before the first use that would instantiate std::array<foo, N>. Otherwise behavior is undefined. So if one translation unit or library uses std::array<foo, N> without including the specialization, you have a problem. Even from a more practical perspective (instead the standard's UB) you are likely to have an ABI break between the libraries/translation units in this case. In other words, the only safe place to put the specialization is in the header that provides foo, which would inside the library.

  2. Your specialization must meet the requirements that the standard puts on a standard library implementation of std::array. One of these requirements is that std::array is an aggregate type. This means that you can't provide a custom constructor to the class, making your goal impossible.

Instead define your own container type that behaves the way you want, although I question what exactly you have in mind. It is likely that what you want is as complex as std::vector and that you would be better served by it. Sometimes a std::vector with a maximum size fully stack allocated is also nice, but the standard library doesn't have that. It can however be emulated with a custom stack allocator together with std::vector.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • 1
    This summarizes what I had in mind too. Especially point 2. It seems impossible to get it right. – Ted Lyngmo Dec 20 '22 at 20:22
  • 2
    Thanks for the answer! 1. Yes I was planning to put array with foo as part of the library. 2. I didn't realize that one of the requirements is that I cannot define a constructor. This is a bummer. I guess I will just resort to implementing a `make_foo_array()` that returns an initializer list of the sentinel type or just create a separate container type. – Ajay Brahmakshatriya Dec 20 '22 at 20:26
  • 2
    @AjayBrahmakshatriya I don't think there are even any fundamentally different ways to implement `std::array` that would be conforming to the standard requirements. It basically must be a wrapper around a built-in array of the given type and size without any special member functions. The best you could argue about is whether additional unused members would be allowed, which would however be pointless. – user17732522 Dec 20 '22 at 20:29
  • 2
    That makes sense. All I wanted was to add a new constructor but I understand that is not allowed. I will just create a separate container. Thanks – Ajay Brahmakshatriya Dec 20 '22 at 20:31
  • I withdraw my _"it seems impossible to get it right"_ comment. I now think it's possible to make the aggregate use a different constructor than the default as I showed in my answer. I'm not sure if that's the only obstacle when it comes to specializing `std::array` though. – Ted Lyngmo Dec 20 '22 at 22:55
2

Disclaimer: I'm not sure if this fails any other requirements on specializations in std:: but it should deal with the aggregate requirement on std::array.


A specialization of std::array for foo (or template <class T> class dyn_var that you mentioned later) would have to be be an aggregate, so you can't add a constructor.

However, aggregates of aggregates don't require extra braces when initializing them due to brace elision. Consider:

template<class T, std::size_t N>
struct array {
    struct inner_array {
        T data[N];
    };
    inner_array m_data;
};

array<int, 3> var{1, 2, 3}; // initializes data in m_data

I think this opens up for a specialization that fulfills the aggregate requirement. Here's my version of dyn_var<T>:

namespace builder {
struct cheap_tag_t {} cheap_tag;

template <class T>
class dyn_var {
public:
    dyn_var() { std::cout << "expensive ctor\n"; }
    dyn_var(T) { std::cout << "T ctor\n"; }

    dyn_var(cheap_tag_t) { std::cout << "cheap ctor\n"; }
};
}  // namespace builder

And one possible std::array specialization that uses the cheap constructor:

template <class T, std::size_t N>
    requires (N != 0)
struct std::array<builder::dyn_var<T>, N> {
    struct inner_array {
        builder::dyn_var<T> data[N];
    };

    inner_array m_data{
        []<std::size_t... I>(std::index_sequence<I...>){
            // RVO:
            return inner_array{((void)I, builder::cheap_tag)...};
        }(std::make_index_sequence<N>())
    };

    // ... member functions ...
};

Now these would both use the cheap constructor:

std::array<builder::dyn_var<int>, 10> foos1;
std::array<builder::dyn_var<int>, 10> foos2{};

while this would use the T ctor for the first 5 elements and the expensive constructor for the remaining elements:

std::array<builder::dyn_var<int>, 10> foos3{1,2,3,4,5};

The last point, that only partially initializing the std::array triggers the default construction of the rest of the elements may be a show stopper since it will certainly confuse the end users, but I thought this may be worth considering nevertheless.

Demo

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • I don't find any requirement that this directly violates, but it is certainly not supposed to satisfy the requirements. Imagine if a standard library decided to implement `std::array` like this... – user17732522 Dec 20 '22 at 23:17
  • @user17732522 You mean with an inner aggregate? It would certainly be unusual, but I'm not sure if anyone would notice. – Ted Lyngmo Dec 20 '22 at 23:49
  • This is an interesting idea! My question is since the member `m_data` is initialized inside the class doesn't that make the constructor non-trivial? And is that allowed by the requirements of `std::array`? Sorry I am having some trouble understanding the exact requirements I guess. – Ajay Brahmakshatriya Dec 20 '22 at 23:54
  • @TedLyngmo No, I think that part is fine. I mean having the default member initializer. I think the relevant requirement in the standard is currently lacking something specifying that the default-initialized `std::array` will default-initialize its elements and equivalently for value-initialization and the remaining elements in aggregate initialization. Otherwise you can't trust how these elements will be initialized. The current requirement for a container says that the default-constructed instance is empty and the part on `std::array` only says that it is non-empty for N>0. – user17732522 Dec 21 '22 at 00:01
  • The container requirement even states that default-initialization must have constant complexity and this is not modified for `std::array`, which seems clearly wrong to me. – user17732522 Dec 21 '22 at 00:02
  • @AjayBrahmakshatriya The `std::array` specification doesn't say anything about that, although I would assume that it should, similar to the above. But if the default constructor of `foo` is non-trivial, then `std::array`'s default constructor is also non-trivial anyway. – user17732522 Dec 21 '22 at 00:06
  • @user17732522 I will turn my answer into a language lawyer question when I finish working. This was far too interesting to leave unanswered :-) – Ted Lyngmo Dec 21 '22 at 06:22
  • @user17732522 Follow-up posted: https://stackoverflow.com/questions/74880400/std-class-specialization-meeting-the-standard-library-requirements-for-the-ori – Ted Lyngmo Dec 21 '22 at 18:35
  • The show-stoppers for specializing `std::array` are the [`iterator` and `const_iterator` typedefs](https://timsong-cpp.github.io/cppwp/n4868/array#overview-5); their exact type is implementation-defined, but they're still part of the `std::array` requirements - i.e. you would need to typedef `iterator` to the same type as the implementation would. msvc for example uses [special array-iterators](https://github.com/microsoft/STL/blob/8ddf4da23939b5c65587ed05f783ff39b8801e0f/stl/inc/array#L417-L418) which your `std::array` specialization would need to use as well. – Turtlefight Dec 21 '22 at 18:37
  • @Turtlefight Yeah, I have reflected on that and I'm not at all sure the specialization needs to use the very same iterator definitions as the standard library implementation uses. The iterators used by the specialization would for sure need to meet the _iterator_ requirements, but that's all (as I've understood it). I may very well be utterly wrong and I'm looking forward to answers (and hints like yours). Please comment under my question too. – Ted Lyngmo Dec 21 '22 at 18:41
  • Another fun thing that you'll have to deal with is that implementations might depend on implementation details of `std::array` - gcc & clang & msvc for example use the internal array for `std::get()` [godbolt](https://godbolt.org/z/WvhTexj5s). So you might need to provide specializations for some (or all) of the support functions as well. – Turtlefight Dec 21 '22 at 18:45
  • I'm not sure if the brace elision will always do the right thing, it becomes wonky when the braces are nested. – HolyBlackCat Dec 21 '22 at 18:46
  • 1
    @Turtlefight Is that really how providing a specialization of a standard library class works? I think not. The interface is specified and the requirements on that should be clear. If the specialization fulfills the interface and storage requirements, why would it need to implement the support functions that that particular library implementation uses? – Ted Lyngmo Dec 21 '22 at 18:49
  • 2
    @TedLyngmo i've re-checked most functions that have specializations for `std::array`, and `std::get` & `std::span` seem to be the only ones that depend on implementation details. So you're probably right that those two should not depend on implementation details. I unfortunately failed to find any CWG issues that would be applicable to specializations of std containers :/ My guess would still be that the implementation-defined typedef's prevent users from specializing `std::array` - i'm looking forward to answers to your follow-up question, maybe someone can solve that mystery :) – Turtlefight Dec 21 '22 at 20:24