enable_if with copy constructors
Asked Answered
C

2

8

I am trying std::enable_if for the first time and struggling. Any guidance would be appreciated.

As a toy example, here is a simple static vector class, for which I want to define a copy constructor, but the behavior depends on the relative sizes of the vectors:

  1. just copy data into a smaller or same-sized vector
  2. copy data into a larger vector and then pad the rest with zeroes

So the vector class is:

template <size_t _Size>
class Vector
{
    double _data[_Size];

public:
    Vector()
    {
        std::fill(_data, _data + _Size, 0.0);
    }

    const double* data() const
    {
        return _data;
    }

    // ...
};

The copy constructor should support something like this: copying the first 2 elements of v3 into v2:

Vector<3> v3;
Vector<2> v2(v3);

I tried a copy constructor for behavior 1. like this, which compiles:

template <size_t _OtherSize,
typename = typename std::enable_if_t<_Size <= _OtherSize>>
Vector(const Vector<_OtherSize>& v) : Vector()
{
   std::copy(v.data(), v.data() + _Size, _data);
}

but the compiler cannot distinguish this from behavior 2. even though the enable_if conditions are mutually exclusive.

template <size_t _OtherSize,
typename = typename std::enable_if_t<_OtherSize < _Size>>
Vector(const Vector<_OtherSize>& v) : Vector()
{
    std::copy(v.data(), v.data() + _OtherSize, _data);
    std::fill(_data + _OtherSize, _data + _Size, 0.0);
}

I also tried putting enable_if in the argument instead, but it couldn't deduce the value of _OtherSize:

template <size_t _OtherSize>
    Vector(const typename std::enable_if_t<_Size <= _OtherSize, 
    Vector<_OtherSize>> & v)
    : Vector()
    {
        std::copy(v.data(), v.data() + _Size, _data);
    }

What is the way to do this (using enable_if, not a simple if statement)?

Chrisoula answered 25/9, 2017 at 11:14 Comment(3)
A copy constructor cannot be a template, by definition. You can have a templated constructor that copies, but it still will not be a copy constructor. ;-]Kitsch
This isn’t the problem, but names that begin with an underscore followed by a capital letter (_Size, _OtherSize) and names that contain two consecutive underscores are reserved for use by the implementation. Don’t use them in your code.Veliger
Unrelated but you don't need typename before std::enable_if_t.Sortie
K
7

Ignoring defaults, the signature of both of those constructors is

template <size_t N, typename>
Vector(const Vector<N>&)

I.e., they're ultimately the same.

One way to differentiate them is to make the template parameter type directly dependent on enable_if's condition:

template <size_t _OtherSize,
    std::enable_if_t<(_Size <= _OtherSize), int> = 0>
    Vector(const Vector<_OtherSize>& v) : Vector()
    {
        std::copy(v.data(), v.data() + _Size, _data);
    }

template <size_t _OtherSize,
    std::enable_if_t<(_OtherSize < _Size), int> = 0>
    Vector(const Vector<_OtherSize>& v) : Vector()
    {
        std::copy(v.data(), v.data() + _OtherSize, _data);
        std::fill(_data + _OtherSize, _data + _Size, 0.0);
    }

As an aside, names like _Size and _OtherSize are reserved for the implementation and thus illegal for user code – lose the underscore and/or the capital letter.

Also, as @StoryTeller hinted at, you don't want the first constructor to apply when _OtherSize == _Size, as the compiler-generated copy constructor has ideal behavior. Said constructor is already less specialized than the copy constructor for same-sized Vectors, so it won't be selected during overload resolution anyway, but it would be best to make the intent clear by switching <= to <.

Kitsch answered 25/9, 2017 at 11:20 Comment(7)
Going towards your comment, <= is problematic, and should really be <. If only to prevent confusion when the "wrong" c'tor is called.Mouthy
@StoryTeller : You mean so the constructor doesn't apply instead of the compiler-generated copy-c'tor? Yes, probably, but I was trying to limit the answer to the code's linguistic issues rather than logical issues (which the OP didn't ask about). Good point though, I'll make a small note of it – thanks!Kitsch
It never will apply during overload resolution after all. There's no risk of ambiguity, other than the OP wondering why their breakpoint isn't being hit :)Mouthy
Where does the proscription on the underscore (e.g. before Size etc.) come from? I have searched a number of books and websites and couldn't find any mention of it. Presumably Size_ is allowed? I am aiming for something that looks like a class name, but is subtly different.Chrisoula
@Chrisoula : C++14 [reserved.names]/2: "If a program declares or defines a name in a context where it is reserved, other than as explicitly allowed by this Clause, its behavior is undefined." and [global.names]/1.1: "Each name that contains a double underscore __ or begins with an underscore followed by an uppercase letter is reserved to the implementation for any use." Wording for other versions of the standard is similar. Size_ is indeed fine, though a bit odd for my tastes. ¯\_(ツ)_/¯Kitsch
Thanks. It is a bit odd for my tastes too, but I am running out of concise options that are not valid class or variable names (by my conventions).Chrisoula
@Chrisoula : Personally, I use the C++ stdlib convention of snake-case everywhere; except for template parameters, which use Pascal-case plus an otherwise-abhorrent form of Hungarian notation of postfixed T, N, or V depending on the form of template parameter. In this case it would result in SizeN and OtherSizeN; since I avoid Hungarian everywhere else, this helps the template parameters stick out like a sore thumb. I'm fond of Rust's stdlib casing, but I just cant get myself away from snake-case for types in C++. :-SKitsch
R
4

Don't use names like _Cap; they are reserved for the implementation. In fact, std source uses these names because they are reserved. Do not mimic std/system header internal naming conventions.

template <size_t O>
Vector(const Vector<O>& v) : Vector()
{
  constexpr auto to_copy = (std::min)( O, Size );
  constexpr auto to_fill = Size-to_copy;
  auto const* src=v.data();
  std::copy(src, src + to_copy, _data);
  std::fill(_data + to_copy, _data + to_copy+to_fill, 0.0);
}
Vector(const Vector& v) = default;

you'll find this optimizes down to the code you want; in the no-fill case, std::fill is called with (foo, foo, 0.0), and the body is a loop that is easy to prove is a no-op by a compiler when passed the same pointer twice.

Resistencia answered 25/9, 2017 at 11:31 Comment(2)
Du you write (std::min) so it is parsed as an expression and std::min is seen as an unqualified-id? If yes, why?Ileum
@Ileum : Avoiding windows.h macro shenanigans is one guess.Kitsch

© 2022 - 2024 — McMap. All rights reserved.