3

Background: The usage of STL makes builds slow due to template code bloat: quite often the same methods are independently instantiated in many different translation units, being compiled and optimized many times. One way to avoid this object code duplication for template classes is to use explicit template instantiation and extern template declaration, but STL implementations do not support them. I'm trying to implement an equivalent of std::vector<T> which would support explicit instantiations.


Question: I have a template class vector<T>, and I want some of its methods to be removed if the template argument T does not satisfy some condition. To make things worse, here are additional requirements:

  1. It must be possible to explicitly instantiate vector<T>, regardless if whether T satisfies the condition or not.
  2. When someone calls a conditionally removed method, a compilation error should be emitted.
  3. I have several such conditions, and a set of methods dependent on each of them.

Discussion: For example, the method vector<T>::push_back(const T&) cannot work when T is not copy-constructible.

If I leave this method implementation intact, then compiler will produce error if I explicitly instantiate e.g. vector<unique_ptr<int>>, because it won't be able to copy unique_ptr<int>. This contradicts requirement 1, and makes the desired code bloat optimization impossible.

I can make two different implementations of vector<T>::push_back(const T&), one for copy-constructible types and one for the others. This can be done using SFINAE overloading or with a helper template class. The implementation for non-copy-constructible case can simply throw exception or call terminate. But then calling the method push_back for vector<unique_ptr<int>> will only crash during runtime. It won't generate compile error, as said in requirement 2.

Finally, there are several conditions, each of them rules out some methods. The type T can lack the following properties in various combinations: copy-constructible, copy-assignable, default-constructible, move-constructible. The solution from the related question seems to work for one condition only, which does not satisfy requirement 3.

The only idea I have left is to use some sort of partial specialization of the whole vector<T> combined with preprocessor hacks. But this would need something like 16 separate specializations, which sounds terrible.

P.S. It becomes pretty obvious to me that STL design is inherently dependent on the mechanics of implicit instantiation, which makes it very hard to reduce the code bloat caused by them.

stgatilov
  • 5,333
  • 31
  • 54
  • Another way would be to use a single translation unit. Also by using "code bloat" people typically refer to poorly designed / incorrectly used templates putting unnecessary burden on compiler, not to template code being instantiated in different translation units. – user7860670 Feb 24 '18 at 13:39
  • Just use SFINAE as you described in the question, but use a static_assert instead of throwing some runtime error. – pschill Feb 24 '18 at 13:52
  • @VTT: Templates in C++ generate code bloat in two dimensions: across template parameters and across translation units. Unity build definitely solves the problem across multiple TU, but makes incremental compilation slow as hell. – stgatilov Feb 24 '18 at 14:11
  • @stgatilov I would not refer to template code duplication across translation units as a "code bloat". In the same manner I would not refer to inline functions code duplication across translation units as a "code bloat". It is just how compilation is. Use of single translation unit typically completely eliminates the need for incremental compilation. – user7860670 Feb 24 '18 at 14:21

2 Answers2

3

You could let overload resolution and std::conditional work its magic. This doesn't suffer from the exponential amount of specializations as each method is predicated only on its own requirements.

template<typename T>
class vector_errors
{
public:
    template<typename...>
    void push_back(const T&)
    {
        static_assert(std::is_copy_constructible_v<T>, "T must be copy constructible");
    }
};

class dummy
{
    dummy(const dummy&) {}  // disable construction
};

template<typename T>
class vector : vector_errors<T>
{
    using base = vector_errors<T>;
public:
    using base::push_back;
    void push_back(const std::conditional_t<std::is_copy_constructible_v<T>, T, dummy>& t)
    {
        if constexpr(std::is_copy_constructible_v<T>)
        {
            // do stuff
        }
    }
};

template class vector<int>;                   // instantiates push_back
template class vector<std::unique_ptr<int>>;  // error on push_back

Live

Passer By
  • 19,325
  • 6
  • 49
  • 96
  • It seems that the key idea here is that you can easily alter the signature of the method depending on condition (using `std::conditional`). 1) Is it true that `vector_errors` is necessary only for better error reporting and it can be discarded? 2) `constexpr if` definitely does wonders. Is it possible to use this solution without it (e.g. in C++11) ? – stgatilov Feb 24 '18 at 16:33
  • @stgatilov `vector_errors` could be discarded, it is only there for errors. The functionality of `if constexpr` is necessary, but could be imitated by specialization, which is hideous when written manually – Passer By Feb 24 '18 at 16:45
  • I managed to get it working without `constexpr if` (see [paste](https://pastebin.com/9hPiwkRx)). Inside `push_back`, simply call a private method `push_back_impl`, which has two template implementations: a real implementation accepting argument of type `T` and an empty method accepting `dummy`. – stgatilov Feb 25 '18 at 06:47
1

You can use SFINAE. It works exactly as per your requirements:

struct X
{
    X() = default;
    X(const X&) = delete;
};


template <class T> struct V
{
    T* ptr_;


    template <class U = T, decltype(U{std::declval<U>()})* = nullptr>
    auto push_back(const T& val)
    {
        static_assert(std::is_same<U, T>::value, "explicit template parameter U provided.");
        // ... code
    }

    // you can have this or not, depending on your preferences
    // auto push_back(...) = delete;
};

// explicit instantiation OK
template struct V<int>;
// you then need to explicit instantiate all the method templates
template auto V<int>::push_back(const int&) -> void;

template struct V<X>; // explicit instantiation OK

// don't instantiate "disabled" methods
// template auto V<X>::push_back(const X&) -> void;


auto test()
{
    V<X> v;  // OK

    v.push_back(X{}); // compile time error
}

You can explicitly instantiate the vector, you can create objects and you get a compile error when calling push_back:

<source>:34:7: error: no matching member function for call to 'push_back'
    v.push_back(X{});
    ~~^~~~~~~~~
<source>:19:10: note: candidate template ignored: substitution failure [with U = X]: excess elements in struct initializer
    auto push_back(const U& val)
         ^

Alternately, if you leave the deleted push_back you get:

<source>:34:7: error: call to deleted member function 'push_back'
    v.push_back(X{});
    ~~^~~~~~~~~
<source>:26:10: note: candidate function has been explicitly deleted
    auto push_back(...) = delete;
         ^
<source>:19:10: note: candidate template ignored: substitution failure [with U = X]: excess elements in struct initializer
    auto push_back(const U& val)
         ^

The only hassle is that you need to explicit instantiate all the method templates, but only those who are not disabled.

bolov
  • 72,283
  • 15
  • 145
  • 224
  • 1
    You lose most of the benefit of explicit instantiation. – T.C. Feb 24 '18 at 13:53
  • As T.C. noted, by making the method template you exclude it from explicit instantiation, thus making the code bloat optimization useless. Compiler instantiates `push_back` only implicitly in your case. – stgatilov Feb 24 '18 at 14:06
  • 1
    @T.C. you are right. Somehow I haven't thought of that. You can explicit instantiate all the methods - which I added to the answer - although that is a bit too much trouble... – bolov Feb 24 '18 at 14:24
  • Yes, this will work. Just be sure to do both explicit instantiation and extern template declaration separately for each of these methods. Hopefully, it can be wrapped into a macro like `EXPLICITLY_INSTANTIATE_VECTOR(T)`. – stgatilov Feb 24 '18 at 14:35
  • On the second thought, writing such a macro is not straightforward, because it must instantiate some methods based on condition. It can be done by using `std::conditional` (instantiate empty method accepting a dummy when condition is false). Anyway, it is probably easier to explicitly instantiate all the methods of the class with the macro, without explicitly instantiating the class itself. – stgatilov Feb 25 '18 at 07:48