1

Hopefully the problem is obvious from the question :) If not, consider the following code:

template <typename Ty>
class test
{
public:
    test(std::string &v) : val(v) {}

    template <typename... Args>
    test(Args&&... args) : val(std::forward<Args>(args)...) {}

    Ty val;
};

int main(void)
{
    std::cout << std::is_assignable<test<std::string>, std::string>::value << std::endl;
    std::cout << std::is_assignable<test<std::string>, std::vector<int>>::value << std::endl;
}

The output is true in both cases, tested using Visual Studio 2013 update 3 as well as ideone. This just seems wrong to me on many different levels. For one, attempting to construct/assign std::vector<int> to an instance of the type test<std::string> will not compile (obviously). I can live with this as the compiler is likely just checking types and whether or not an acceptable function exists (and not whether or not said function compiles).

What bugs me the most is that constructors should have no effect on assignment. This seems to be true (at least in GCC) when removing the variadic constructor. The output is then false in both cases. On Visual Studio, the output is still true for the first line of output; I'll file a bug on that tomorrow.

Anyway, is this correct behavior? It seems quite counter-intuitive.

Duncan
  • 980
  • 6
  • 17
  • 2
    It's correct; your variadic constructor essentially is defining an implicit conversion from *everything*. – T.C. Aug 15 '14 at 08:00
  • use `explicit` for your variadic constructor to prevent implicit conversion. – foobar Aug 15 '14 at 08:02

2 Answers2

4

This doesn't have much to do with variadic constructor templates. The problem is that you have a constructor template taking an unconstrained universal reference parameter. Using the following constructor also prints 1 in both cases:

template <typename Args>
test(Args&& args) : val(std::forward<Args>(args)) {}

The definition of is_assignable is (§20.10.4.3 [meta.unary.prop], Table 49):

The expression declval<T>() = declval<U>() is well-formed when treated as an unevaluated operand (Clause 5). Access checking is performed as if in a context unrelated to T and U. Only the validity of the immediate context of the assignment expression is considered. [ Note: The compilation of the expression can result in side effects such as the instantiation of class template specializations and function template specializations, the generation of implicitly-defined functions, and so on. Such side effects are not in the “immediate context” and can result in the program being ill-formed. —end note ]

This allows implicit conversion sequences to be used. Universal references can bind to everything, so your variadic constructor defines implicit conversions from everything to test. (Your constructor taking a std::string & doesn't have an effect on is_assignable because the return type of declval<std::string>() is an rvalue reference and so doesn't bind to non-const lvalue references.)

If you don't want this to happen, mark your constructor explicit. It's probably also a good idea to constrain your variadic template constructor using std::is_constructible:

template <typename... Args, typename = typename std::enable_if<std::is_constructible<Ty, Args...>::value>::type>
explicit test(Args&&... args) : val(std::forward<Args>(args)...) {}
T.C.
  • 133,968
  • 17
  • 288
  • 421
2

Constructors affect assignment via the implicit copy assignment operator:

test& operator=(test const&) = default;

The effect is:

test<std::string> a;
std::string b;
std::move(a) = std::move(b);
// a.operator=(b) constructs a temporary test<std::string> from b

If you delete the copy assignment operator, then is_assignable will become false in both cases.

An alternative is to mark your converting constructors explicit; this will prevent them being available to convert the RHS operand of the assignment operator.

To correct the determination of the compiler on your variadic template constructor, you can use decltype SFINAE:

template <typename... Args,
    typename = decltype(Ty(std::forward<Args>(std::declval<Args>())...))>
test(Args&&... args) : val(std::forward<Args>(args)...) {}

The difference in behaviour between gcc and Visual Studio is a non-standard extension on the part of the latter compiler: rvalue to lvalue conversion Visual Studio. This allows the lvalue reference in the std::string& constructor to bind to the xvalue std::string returned from std::declval<std::string> in the definition of is_assignable. gcc will give the same result if you pass std::string& to is_assignable or alternatively if you make that constructor take a const lvalue reference or a rvalue reference.

Community
  • 1
  • 1
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • The first part of your response tends to suggest that `std::is_assignable, std::string>::value` being false (after removing the variadic constructor) is a bug in GCC, and not Visual Studio – Duncan Aug 15 '14 at 08:29
  • @Duncan well, `is_assignable` is written in terms of `declval`, so if you want to test lvalue assignment you should pass reference types to it. I'll clarify my answer. – ecatmur Aug 15 '14 at 08:31
  • Sure, but it should still be consistent across compilers. In fact, I found that `test x; x = std::string("foo");` does not compile in GCC, but does in Visual Studio. One of them must be wrong :) – Duncan Aug 15 '14 at 08:35
  • VC++ is known to be a bit buggy binding prvalues (`std::string("foo")`) to lvalue references (`test(std::string&)`). Is there a reason that constructor can't take a const reference or rvalue reference? – ecatmur Aug 15 '14 at 08:40
  • At this point, I'm mostly just curious about the inconsistencies between the compilers. This code doesn't actually resemble real code at all and was only formed to simplify the issue I ran into. – Duncan Aug 15 '14 at 08:47
  • @Duncan it's a non-standard extension. See http://stackoverflow.com/questions/11508607/rvalue-to-lvalue-conversion-visual-studio – ecatmur Aug 15 '14 at 08:49