Скрытие части реализации мэйкера для шаблонного класса

Имеется некая библиотека, реализующая шаблон TDerived основанный на интерфейсе IBase. В ней есть метод-мэйкер MakeObj, которой может вызвать соответствующий аргументу конструктор в run-time.

// header_1.h
#pragma once

#include <memory>
#include <array>

template<typename T>
struct IBase
{};

template <typename T, unsigned char Num_>
struct TDerived : IBase<T>
{};

template<typename T>
using FMaker = std::unique_ptr<IBase<T>>(*)();

template<typename T, unsigned char num>
std::unique_ptr<IBase<T>> MakeObj()
{
    return std::unique_ptr<IBase<T>>{new TDerived<T, num>{}};
}

template<typename T, unsigned char... num_pack>
constexpr std::array<FMaker<T>, 256>
MakeLut(::std::integer_sequence<unsigned char, num_pack...>)
{
    return std::array<FMaker<T>, 256>{ &MakeObj<T, num_pack>..., &MakeObj<T, 255> };
}

template<typename T>
constexpr std::array<FMaker<T>, 256> lut{ MakeLut<T>(::std::make_integer_sequence<unsigned char, 255>()) };

template<typename T>
std::unique_ptr<IBase<T>> NewObj(unsigned char num)
{
    return (*lut<T>[num])();
}

Пример вызова

// main.cpp
#include "header_1.h"

int main()
{
    int num = 5;
    auto ptr = NewObj<int>(num);
}

Замечательно работает.

Теперь, я хочу скрыть часть реализации мэйкера. Сделать недоступными для прямого обращения некоторые вспомогательные методы. Если бы не шаблонный параметр typename T, я бы легко вынес часть реализации в другую единицу трансляции.

// header_1.h
#pragma once

#include <memory>
#include <array>

template<typename T>
struct IBase
{};

template <typename T, unsigned char Num_>
struct TDerived : IBase<T>
{};

template<typename T>
using FMaker = std::unique_ptr<IBase<T>>(*)();

template<typename T>
extern constexpr std::array<FMaker<T>, 256> lut;

template<typename T>
std::unique_ptr<IBase<T>> NewObj(unsigned char num)
{
    return (*lut<T>[num])();
}
//source_1.cpp
#include "header_2.h"

template<typename T, unsigned char num>
std::unique_ptr<IBase<T>> MakeObj()
{
    return std::unique_ptr<IBase<T>>{new TDerived<T, num>{}};
}

template<typename T, unsigned char... num_pack>
constexpr std::array<FMaker<T>, 256>
MakeLut(::std::integer_sequence<unsigned char, num_pack...>)
{
    return std::array<FMaker<T>, 256>{ &MakeObj<T, num_pack>..., & MakeObj<T, 255> };
}

template<typename T>
constexpr std::array<FMaker<T>, 256> lut{ MakeLut<T>(::std::make_integer_sequence<unsigned char, 255>()) };

Но так это, конечно же, не работает. Линковщик ругается

Ошибка LNK2001 неразрешенный внешний символ "class std::array<class std::unique_ptr<struct IBase,struct std::default_delete<struct IBase > > (__cdecl*)(void),256> const lut" (??$lut@H@@3V?$array@P6A?AV?$unique_ptr@U?$IBase@H@@U?$default_delete@U?$IBase@H@@@std@@@std@@XZ$0BAA@@std@@B).

Есть ли какой-нибудь способ заставить компилироваться вторую единицу трансляции зависимо от первой? Пусть будет перекомпилироваться повторно, не страшно. Или реализация функционала в заголовочных файлах для шаблонов - это приговор?

Подобные темы уже поднимались на SO. Вот, например. Но все они очень старые, и я решил, что можно и спросить хотя бы в контексте мэйкеров. Извините, если что.

Добавлено

Под "заставить компилироваться вторую единицу трансляции зависимо от первой" имеется в виду что-то типа: Если добавить в source_1.cpp

template<>
constexpr std::array<FMaker<int>, 256> lut<int>{ MakeLut<int>(::std::make_integer_sequence<unsigned char, 255>()) };

то вызов auto ptr = NewObj<int>(num); уже работает. Может быть как-то с помощью макросов такое можно сделать? Чтобы не вручную для каждого типа.


Ответы (1 шт):

Автор решения: sibedir

Вот вчера всю голову сломал, а сейчас написал вопрос и само в голову решение пришло )))

Сделать мэйкер частью реализации базового класса, и в нём уже скрыть вспомогательные методы

// header_1.h
#pragma once

#include <memory>
#include <array>

template<typename T>
struct IBase;

template <typename T, unsigned char Num_>
struct TDerived : IBase<T>
{};

template<typename T>
struct IBase
{
private:
    using FMaker = std::unique_ptr<IBase>(*)();

    template<unsigned char num>
    static std::unique_ptr<IBase<T>> MakeObj()
    {
        return std::unique_ptr<IBase<T>>{new TDerived<T, num>{}};
    }

    template<unsigned char... num_pack>
    static constexpr std::array<FMaker, 256>
        MakeLut(::std::integer_sequence<unsigned char, num_pack...>)
    {
        return std::array<FMaker, 256>{ &MakeObj<num_pack>..., & MakeObj<255> };
    }

    static constexpr std::array<FMaker, 256> lut{ MakeLut(::std::make_integer_sequence<unsigned char, 255>()) };

public:
    static std::unique_ptr<IBase<T>> GetNew(unsigned char num)
    {
        return (*lut[num])();
    }
};

Теперь обратиться к MakeObj, MakeLut и lut нельзя. Только через GetNew

// main.cpp
#include "header_1.h"

int main()
{
   int num = 5;
   auto ptr = IBase<int>::GetNew(num);

   //IBase<int>::MakeObj<5>(); // Error
}

Но может у кого-то будет идея получше ?

→ Ссылка