Early instantiation of non-template method of template class

Viewed 152

Consider the following class:

#include <vector>

template<class T>
struct my_struct {
    my_struct() {}
    explicit my_struct(T) {}
    explicit my_struct(std::vector<T>) {}
};

int main() {    
    my_struct<const int> s1(1);
}

The use case is that the third constructor should only be called in some instanciations of my_struct, and not in some other cases (e.g. with const types). It is what is done here in the main: it is not called.

I get a compiler error (GCC, Clang, Intel). GCC error message : /usr/local/include/c++/10.2.0/bits/stl_vector.h:401:66: error: static assertion failed: std::vector must have a non-const, non-volatile value_type

In fact, it has nothing to do with std::vector (but the compilers are pretty bad at giving us any clue). It can be reproduced with a very similar code:

class my_class {
    static_assert(0, "should not be used");
};

template<class T>
struct my_struct {
    my_struct() {}
    explicit my_struct(T) {}
    explicit my_struct(my_class) {}
};

int main() {
    my_struct<int> s(1); // error
}

The problem, as I interpret it, is that once my_struct is instanciated, it results in a concrete type, and the compiler may instanciate all its methods. In this case, it instanciates my_struct(my_class), so it instanciates my_class, so we get the static error.

note: we might get "lucky" however: in the vector case, calling only my_struct() does not trigger the error (something to do with overload resolution I guess)

note: the way I fix the error is by templating the constructor instead of using vector:

template<class vector_type>
my_struct(vector_type) {}

I find it ugly but have no other idea.

  1. Is my interpretation of what is going on correct?
  2. Is this behavior required / left undefined by the language? Or is it a compiler bug?
  3. If yes, I would call it a language defect in C++17 at least. Would you agree?
  4. If yes, is there a language defect report? Is it corrected in C++20?
  5. If no: how would you solve the problem? Is it a design problem?
  6. Could we expect the compilers to have, in a not too distant future, a more detailed stack trace of what is going on? At least mentionning why it needs to instanciate a type that is never called by the concrete class at hand.
2 Answers

As of C++20, you can make use of constraint expressions as introduced by a trailing requires clause to constrain the instantiation of the third constructor for a given instantiation of the my_struct class template to some predicate on the type template parameter T of the class template:

#include <type_traits>
#include <vector>

template<class T>
struct my_struct {
    my_struct() {}
    explicit my_struct(T) {}
    explicit my_struct(std::vector<T>) requires (!std::is_const_v<T>) {}
};

int main() {
    my_struct<const int> s(1);
    my_struct<int> t({1, 2});
}

Other than using a less complex syntax as compared to the pre-C++20 std::enable_if_t SFINAE approach, it also doesn't force you to make the constructor a template function solely such that the constructor itself is parameterized over a type template parameter that participates in the deduction process when the constructor is a candidate function in overload resolution.

Particularly, [temp.constr.constr]/2 states [emphasis mine]:

[temp.constr.constr]/2

In order for a constrained template to be instantiated ([temp.spec]), its associated constraints shall be satisfied as described in the following subclauses. [Note: Forming the name of a specialization of a class template, a variable template, or an alias template ([temp.names]) requires the satisfaction of its constraints. Overload resolution requires the satisfaction of constraints on functions and function templates. — end note]

where the particular associated constraint in this case is covered by [temp.constr.decl]/3.3.4:

[temp.constr.decl]/3

A declaration's associated constraints are defined as follows:

  • [...]
  • (3.3) Otherwise, the associated constraints are the normal form of a logical AND expression whose operands are in the following order:
    • [...]
    • (3.3.4) the constraint-expression introduced by a trailing requires-clause ([dcl.decl]) of a function declaration ([dcl.fct]).

You can use std::enable_if to disable the vector constructor when T is const:

#include <type_traits>
#include <vector>

template<class T>
struct my_struct {
    my_struct() {}
    explicit my_struct(T) {}
    template <typename U = T>
    explicit my_struct(std::vector<U>, std::enable_if_t<!std::is_const_v<U>, int> = 0) {}
};

int main() {
    my_struct<const int> s(1);
    my_struct<int> t({1, 2});
}
Related