19

Should the type trait be able to handle cases such as std::vector < std::unique_ptr <int> > and detect that it's not copy constructible?

Here's an example at https://ideone.com/gbcRUa (running g++ 4.8.1)

#include <type_traits>
#include <vector>
#include <iostream>
#include <memory>

int main()
{
    // This prints 1, implying that it's copy constructible, when it's clearly not
    std::cout << std::is_copy_constructible< std::vector<std::unique_ptr<int> > >::value << std::endl; 
    return 0;
}

If this is the correct behavior for is_copy_constructible, is there a way to detect that the copy construction is ill formed? Well, beyond just having it fail to compile.

MSalters
  • 173,980
  • 10
  • 155
  • 350
Dave S
  • 20,507
  • 3
  • 48
  • 68
  • 1
    Sadly, yes, this is allowed behaviour :(. I went on about this issue in general and why we need to use SFINAE to prevent it, here: http://flamingdangerzone.com/cxx11/2013/02/11/to-sfinae-or-not-to-sfinae – R. Martinho Fernandes Aug 23 '13 at 13:33
  • @R.MartinhoFernandes: Bleh, I was afraid you were going to say that. I hadn't found the `is_constructible` question before, but it's the same answer, since `is_copy_constructible` refers to `is_constructible`. – Dave S Aug 23 '13 at 14:02
  • 2
    yeah, it's quite annoying. Your question prompted me to bring the issue to the standard proposals mailing list: https://groups.google.com/a/isocpp.org/d/msg/std-proposals/pgWYGIvV2Tw/8OteiWGUXfwJ. I should have probably done so back in February as the deadline for C++14 proposals is 30th August. – R. Martinho Fernandes Aug 23 '13 at 14:09
  • @R.MartinhoFernandes: Urgh, I thought we could work around this using `decltype` and an explicit check but [apparently not](http://ideone.com/72F5DF), is this also a defect you were aware of ? – Matthieu M. Aug 23 '13 at 15:02
  • @MatthieuM. Yeah, that's essentially the same problem: `decltype` doesn't look past the immediate context either. There is really no way around it that doesn't involve changes to `vector` :( – R. Martinho Fernandes Aug 23 '13 at 15:05
  • @R.MartinhoFernandes: Actually, I am rather disappointed that `decltype` does not perform a full instantiation. The idea of having a simulator is awesome (and could supersedes all traits actually, or be used to write them naturally), but here it seems crippled for no good reason :x – Matthieu M. Aug 23 '13 at 15:09
  • @MatthieuM. And the worst is that, even if you are excruciatingly careful to SFINAE out all your interfaces when appropriate, you get traits that don't "lie", but you get horrible, horrible errors everywhere, while `static_assert` would give you nice errors but lying traits. – R. Martinho Fernandes Aug 23 '13 at 15:16
  • This http://chat.stackoverflow.com/transcript/10?m=10812818#10812818 sums up my feelings about this matter. – R. Martinho Fernandes Aug 23 '13 at 15:17

3 Answers3

20

This is because of a flaw in the design of std::vector. std::vector defines copy construction even if it will fail to compile, and relies on users of std::vector to not invoke the method if it will fail to compile.

The alternative design would be to SFINAE block the invocation of the method if the type contained in the vector does not have a copy constructor. However, std::vector was designed before modern SFINAE techniques developed.

It could possibly be retro fitted into a new iteration of C++, as there would be very little code that would break. One cannot say no code would break, because you could have code that relies on the fact that std::is_copy_constructible< std::vector< no_copy_type > > is std::true_type, or equivalent expressions, but that is a pretty strange dependency.

On top of the fact that std::vector is older than the SFINAE techniques that could solve this problem, doing so with SFINAE is pretty messy (as SFINAE is a messy technique). The new concepts-lite proposed for C++1y may make it cleaner, and more tempting to include in a new iteration of the language.

My work around when I have a container that needs to know if the contained object can be safely copied, compared and ordered is to specialize for std::vector on a custom traits class, and fall back on the value of the custom traits class on the contained type. This is a patchwork solution, and quite intrusive.

template<template<typename>class test, typename T>
struct smart_test : test<T> {};
template<template<typename>class test, typename T, typename A>
struct smart_test<test, std::vector<T,A>> : smart_test<T> {};

which gives us:

template<typename T>
using smart_is_copy_constructible = smart_test< std::is_copy_constructible, T >;

and similar for < and ==. I can add more specializations when I run into more container-types that should really forward their properties down to their data, or I could write a fancier SFINAE container-test and traits and extract the underlying value-type and dispatch the question to the test on the value-type.

But in my experience, I mostly end up doing these tests on std::vector.

Note that since vector has had "participates in overload resolution" rules added to it, which is standards-speak for "do SFINAE" tests.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Well, you've given me an interesting work around. I had already thought of adding a specialized template for `is_copy_constructible`, but I like the fact that you've given me one that I can reuse on several type traits. – Dave S Aug 23 '13 at 14:50
  • As posted, the code does not compile [and not only because of the missing semi-colon]. – degski Sep 28 '19 at 10:48
  • @degski Technically *it* compiles, you just need to separate it from the next statement or declaration. ;) – Yakk - Adam Nevraumont Sep 28 '19 at 13:24
  • @Yakk-AdamNevraumont I don't understand what you mean with 'technically it compiles', practically my compiler says 'it's no good'. – degski Sep 30 '19 at 14:00
  • @degski `"hello"` is a compling C++ string, but if you fed a program containing nothing but `"hello"` the compiler would complain; you need to contain `"hello"` in code on either side. Similarly, mine works, technically, so long as the stuff around it had a `;` after it. ;) – Yakk - Adam Nevraumont Sep 30 '19 at 14:02
3

I want to clarify something said by the accepted answer (the one from @Yakk - Adam Nevraumont).

If std::vector doesn't properly delete the copy constructor for non-copyable types, it is not because of a design flaw or a lack of modern SFINAE techniques, it's because those who created it wanted to be able to instantiate a vector of an incomplete type.

Containers can either properly SFINAE their special members, or support incomplete types. There is no good or wrong choice, both of them have their benefits, for more details about that you can check this article which go more in depth: https://quuxplusone.github.io/blog/2020/02/05/vector-is-copyable-except-when-its-not/.

Since C++17 std::vector is required to be instantiable with incomplete types if the allocator allows it (the default one does):

[vector.overview]

An incomplete type T may be used when instantiating vector if the allocator meets the allocator completeness requirements. T shall be complete before any member of the resulting specialization of vector is referenced.

[allocator.requirements.completeness]

If X is an allocator class for type T, X additionally meets the allocator completeness requirements if, whether or not T is a complete type:

  • X is a complete type, and
  • all the member types of allocator_­traits other than value_­type are complete types.

[default.allocator]

All specializations of the default allocator meet the allocator completeness requirements ([allocator.requirements.completeness]).

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Franck W.
  • 31
  • 5
2

Table 49 of the C++11 standard lists what conditions a class has to fulfil for is_copy_constructable<T>::value to be true, and that's unfortunately not much:

is_constructable<T, const T&>::value is true

So if std::vector<T> has a copy constructor it passes the test.

nikolas
  • 8,707
  • 9
  • 50
  • 70