Jay
Jay

Reputation: 75

Is partial specialization in a cpp file not "well-formed"

This a follow-up to [question]: No generated code for explicitly specialized template even with explicit instantiation.

I am using partial specializations in a .cpp file to handle special cases while not forcing all the code into a header file. A simplified example of what I'm doing is given below. This works with both gcc and clang, but given the answer to the question above, I'm wondering if I'm just getting lucky that the linker is finding compatible symbols.

I'm starting with a class template defined in foo.hpp:

#pragma once

template <typename T1, typename T2>
class foo
{
public:
  foo (T1 t1, T2 t2) : d_t1(t1), d_t2(t2) {}
  ~foo () = default;

  void print ();
private:
  T1 d_t1;
  T2 d_t2;
};

Note that the print() method is declared, but not defined.

Now in the foo.cpp, I define the print() method.

#include <iostream>

template <typename T1, typename T2>
void
foo<T1,T2>::print()
{
    std::cout << "T1 is: ";
    d_t1.print();
    std::cout << std::endl;
    std::cout << "T2 is: ";
    d_t2.print();
    std::cout << std::endl << std::endl;
}

Note that this implementation assumes that both T1 and T2 will have a method called print(). In my real world code, the types which are used as arguments to this template usually conform to this interface, but sometimes they don't and I deal with that through partial specialization in the .cpp file. This template is not meant for really generic usage, it only needs to work with a small number of types which I know up-front.

So for instance, I might have 3 types such as

class HasPrint
{
public:
  HasPrint () = default;
  ~HasPrint () = default;

  void print ();

};

class AlsoHasPrint
{
public:
  AlsoHasPrint () = default;
  ~AlsoHasPrint () = default;

  void print ();

};

class NoPrint
{
public:
  NoPrint () = default;
  ~NoPrint () = default;

  void noPrint ();

};

Note that 'HasPrint' and 'AlsoHasPrint' classes have a print() method, while the 'NoPrint' class has a noPrint() method. In the bodies of these classes the print/noPrint methods simply print the name of the class.

To handle the case of someone using NoPrint as one of the template arguments, I define a partial specialization in the foo.cpp file:

template <typename T2>
class foo <NoPrint, T2>
{
public:
  foo (NoPrint n1, T2 t2) : d_n1(n1), d_t2(t2) {}
  ~foo () = default;

  void print ()
  {
    std::cout << "NoPrint is: ";
    d_n1.noPrint();
    std::cout << std::endl;
    std::cout << "T2 is: ";
    d_t2.print();
    std::cout << std::endl;
  }
private:
  NoPrint d_n1;
  T2 d_t2;
};

Then to get all the code I need generated for all the permutations that I use, I include (in the foo.cpp file) the following explicit instantiations of foo.

template class foo<HasPrint, HasPrint>;
template class foo<HasPrint, AlsoHasPrint>;
template class foo<AlsoHasPrint, AlsoHasPrint>;
template class foo<NoPrint, HasPrint>;

The result of the last explicit instantiation is that code is generated for an expansion of the template using the NoPrint object and calling the method noPrint().

In a separate test.cpp file, I have the driver for my test program.

#include "foo.hpp"
#include "hasprint.hpp"
#include "alsohasprint.hpp"
#include "noprint.hpp"

int
main (int argc, char** argv)
{
  HasPrint h1;
  HasPrint h2;
  AlsoHasPrint a1;
  NoPrint n1;
  foo<HasPrint, HasPrint> f1 (h1, h2);
  foo<HasPrint, AlsoHasPrint> f2 (h1, a1);
  foo<AlsoHasPrint, AlsoHasPrint> f3 (a1, a1);
  foo<NoPrint, HasPrint> f4 (n1, h1);


  f1.print();
  f2.print();
  f3.print();
  f4.print();
}

This works as expected on both gcc 4.8.3 and clang 3.2. The result is:

T1 is: HasPrint
T2 is: HasPrint

T1 is: HasPrint
T2 is: AlsoHasPrint

T1 is: AlsoHasPrint
T2 is: AlsoHasPrint

NoPrint is: NoPrint
T2 is: HasPrint

This keeps the header for foo.hpp nice and clean at the expense of needing to use explicit instantiation for each set of types for which I use the template, and a partial specialization if one of the argument types does not conform to the interface.

Given that I'm using the type Foo< NoPrint, HasPrint > in the test driver, without informing the compiler that a specialization exists, is this "well-formed" or am I just getting lucky?

Thanks

Upvotes: 2

Views: 210

Answers (1)

Barry
Barry

Reputation: 303537

Your code is ill-formed, no diagnostic required, according to [temp.class.spec]:

A partial specialization shall be declared before the first use of a class template specialization that would make use of the partial specialization as the result of an implicit or explicit instantiation in every translation unit in which such a use occurs; no diagnostic is required.

That said, for such a case, you don't need a partial specialization at all, and can simply forward along your members:

// from the primary
void print() {
    print("T1", d_t1);
    print("T2", d_t2);
}

Where:

// has print()
template <typename T,
          typename = std::enable_if_t<has_print<T>::value>>
void print(const char* name, const T& val) {
    std::cout << name << " is: ";
    val.print();
    std::cout << std::endl;
}

// has noPrint()
template <typename T,
          typename = std::enable_if_t<has_noprint<T>::value>>
void print(const char* /* unused */, const T& val) {
    std::cout << "NoPrint: ";
    val.noPrint();
    std::cout << std::endl;
}

I'll leave the implementation of those type traits as an exercise to the reader. For a guide to several different ways of how to write such a thing, see this question, with solutions involving std::experimental::is_detected, void_t, overload resolution with trailing-return-type, can_apply, and REQUIRES.

Upvotes: 3

Related Questions