2

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.
Bérenger
  • 2,678
  • 2
  • 21
  • 42
  • 3
    1) `static_assert(0, "should not be used");` is [invalid per se](https://stackoverflow.com/questions/14637356/static-assert-fails-compilation-even-though-template-function-is-called-nowhere), and this is unrelated to `std::vector`. 2) I suggest that you also the add language standard tag. E.g., C++17 and C++20 solutions would be different. – Evg Sep 09 '20 at 11:23
  • I once concluded that an implementation is within its right to eagerly [instantiate a standard library template](https://stackoverflow.com/questions/63061188/what-are-the-rules-for-standard-library-containers-and-incomplete-types) since its constraints impact program validity. You have a similar case, it would seem. – StoryTeller - Unslander Monica Sep 09 '20 at 11:35
  • Although, GCC has no qualms about the vector when it's passed by reference. So there's that. – StoryTeller - Unslander Monica Sep 09 '20 at 11:40
  • @Evg Added both C++17 and C++20 because I would like the answer for both obviously – Bérenger Sep 09 '20 at 12:07
  • @StoryTeller-UnslanderMonica Yes but I want to pass as a sink argument in order to move, so a reference is not an option – Bérenger Sep 09 '20 at 12:07
  • Hmmm. [The implicit instantiation of a class template specialization causes the implicit instantiation of the declarations, but not of the definitions, ... of the class member functions ...](https://timsong-cpp.github.io/cppwp/n4659/temp.inst#2) and incomplete types are allowed in function declarations. However, both GCC and Clang complain about incomplete types in parameters of a class template's member functions. (Even though Clang accepts non-member function declarations with incomplete types in parameters). – Language Lawyer Sep 09 '20 at 12:19
  • @LanguageLawyer But since vector is taken by value, doesn't it have to be a complete type even if we were to look only at the declaration ? – Bérenger Sep 09 '20 at 12:47
  • @LanguageLawyer so, regardless of my previous comment, you would rather lean towards GCC and Clang being too eager when instanciating methods? – Bérenger Sep 09 '20 at 12:50
  • _But since vector is taken by value, doesn't it have to be a complete type even if we were to look only at the declaration ?_ No, a type doesn't need to be complete in a function declaration parameters, only in definition ones. And class template need not be instantiated if the complete type is not required. To my understanding, `vector` need not be instantiated. – Language Lawyer Sep 09 '20 at 12:51
  • _However, both GCC and Clang complain about incomplete types in parameters of a class template's member functions_ BTW, it seems they are allowed, but the corresponding issue is not officially resolved yet. – Language Lawyer Sep 09 '20 at 13:46
  • @LanguageLawyer *the corresponding issue is not officially resolved yet* Do you have a GCC/Clang issue where they talk about it? You can post it as an answer I think – Bérenger Sep 09 '20 at 14:34
  • It is a CWG issue. And it is not 100% related, because I meant non-dependent incomplete type. – Language Lawyer Sep 09 '20 at 14:37
  • @LanguageLawyer CWG? – Bérenger Sep 09 '20 at 15:02
  • 2
    @LanguageLawyer: The parameter type must be instantiated to perform overload resolution since it might be constructible from the argument type. (With a reference, the implementation is allowed to skip instantiation ([temp.inst]/9).) – Davis Herring Sep 10 '20 at 02:38
  • @DavisHerring indeed. – Language Lawyer Sep 10 '20 at 02:50

2 Answers2

4

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]).
dfrib
  • 70,367
  • 12
  • 127
  • 192
1

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});
}
Alan Birtles
  • 32,622
  • 4
  • 31
  • 60