【问题标题】:Specialize the base's templated member function in derived class在派生类中特化基类的模板化成员函数
【发布时间】:2017-02-10 01:29:20
【问题描述】:

TL;DR;

以下代码的编译以两个未解析的外部结束。

问题

有没有办法在基类中有一个未定义的模板化成员函数,并让该函数在派生类中部分特化,这样部分特化将被限制在定义它的派生类中?

解释

如您所见,serial_portliquid_crystal 都派生自 stream 基类。 stream 类将提供一个统一的接口来将文本发送到不同的外围设备。从stream 派生的每个类都必须实现print(char) 函数,该函数将处理与外围设备的低级通信。除此之外,还有一个未定义的、模板化的 print 版本,可以专门用于用户可能想要打印的任何自定义类型。

stream 类具有operator << 的模板定义,用于将数据写入流。这个操作员会调用stream::print 来处理实际的打印。如您所见,print(const char*)print(fill) 已经定义,因为我希望它们出现在来自stream 的每个派生类中。

现在是引入错误的部分

我想与之通信的外围设备有一些基本命令(LCD:将光标移动到 x,y 坐标,串行端口:将波特率设置为 x)它们之间不可互换,这意味着 LCD 不知道如何更改波特率和串口没有可以移动到特定坐标的光标。我想通过operator << 传递命令,就像我对fill 所做的那样。每个命令都将是一个新结构,其中包含命令所需的数据,并且将有一个专门的 print 版本来处理每个命令。

这在理论上可行,但在编译过程中失败,因为print 的专用版本是在派生类中定义的,而operator << 是在stream 类中定义的。当我将命令传递给流时,链接器会在 stream 中查找 print 的专门定义,当然它会失败,因为那些根本不存在。

我该如何克服这个错误? 我正在使用 Visual Studio 15 Preview 4,并且没有更改任何编译器标志。

源代码

#include <iostream>

class stream
{
public:
    struct fill
    {
        int n;
        char ch;
    };

    stream()
    {}

    virtual ~stream()
    {}

    template <typename T>
    stream& operator << (T t)
    {
        this->print(t);
        return *this;
    }

protected:
    virtual void print(char) = 0;

    template <typename T>
    void print(T);
};

template <>
void stream::print<const char*>(const char* str)
{
    while (*str != '\0')
        this->print(*(str++));
}

template <>
void stream::print<stream::fill>(stream::fill f)
{
    while (f.n > 0)
    {
        this->print(f.ch);
        f.n--;
    }
}

class serial_port : public stream
{
public:
    struct set_baudrate
    {
        int baud;
    };

    using stream::stream;

private:
    void print(char c) override
    {
        // TODO: print to the actual serial port
        std::cout << c;
    }

    template <typename T>
    void print(T t)
    {
        stream::print<T>(t);
    }
};

template <>
void serial_port::print<serial_port::set_baudrate>(serial_port::set_baudrate)
{
    this->print("set_baudrate");
}

class liquid_crystal : public stream
{
public:
    struct move
    {
        int x;
        int y;
    };

    using stream::stream;

private:
    void print(char c) override
    {
        // TODO: print to a character LCD
        std::cout << c;
    }

    template <typename T>
    void print(T t)
    {
        stream::print<T>(t);
    }
};


template <>
void liquid_crystal::print<liquid_crystal::move>(liquid_crystal::move)
{
    this->print("move");
}

int main()
{
    liquid_crystal lcd;
    lcd << liquid_crystal::move{ 1, 2 };
    serial_port serial;
    serial << serial_port::set_baudrate{ 9600 };
}

编辑

查看compiler output,问题更加明显,链接器正在寻找void stream::print&lt;liquid_crystal::move&gt;(liquid_crystal::move)void stream::print&lt;serial_port::set_baudrate&gt;(serial_port::set_baudrate),但函数签名应该是void liquid_crystal::print&lt;liquid_crystal::move&gt;(liquid_crystal::move)void serial_port::print&lt;serial_port::set_baudrate&gt;(serial_port::set_baudrate)

【问题讨论】:

  • 您使用什么编译器和标志?不是these
  • 我会立即编辑帖子
  • @LogicStuff 我更新了我的问题和源代码。 Here 是编译器输出,现在它产生与 VS 中相同的错误。
  • 嗯... GCC 明确指出问题出在operator&lt;&lt;(),这使得缩小范围更容易:In function `stream&amp; stream::operator&lt;&lt; &lt;liquid_crystal::move&gt;(liquid_crystal::move)': main.cpp:(.text._ZN6streamlsIN14liquid_crystal4moveEEERS_T_[_ZN6streamlsIN14liquid_crystal4moveEEERS_T_]+0x1f): undefined reference to `void stream::print&lt;liquid_crystal::move&gt;(liquid_crystal::move)'
  • 出于好奇,您是否有理由不能制作一些可以传递给operator&lt;&lt; 的操纵器函数,类似于std::endl()

标签: c++ templates inheritance


【解决方案1】:

main 中,一行:

lcd << liquid_crystal::move{ 1, 2 };

来电:

stream::operator<< <liquid_crystal::move>(liquid_crystal::move)

然后调用:

stream::print<liquid_crystal::move>(liquid_crystal::move)

stream::print 函数仅针对const char*stream::fill 类型定义。

不使用liquid_crystal::print 函数,因为它不是stream::print 的覆盖(它在liquid_crystal 类中隐藏stream::print)。为了使用this(这是stream*,而不是liquid_crystal*)从stream 访问它,stream::print 必须是虚拟的。但在这种情况下,这是不可能的,因为它是一个模板函数。

一般来说,虚拟模板功能设计问题的解决方案通常并不简单。但在这种特定情况下,最简单的方法可能是在每个派生类中复制operator&lt;&lt;。但是,在调用stream::operator&lt;&lt;的情况下它不起作用,例如:

void func(stream& s) {
    s << liquid_crystal::move{ 1, 2 };
}
int main() {
    liquid_crystal lcd;
    func(lcd);
}

【讨论】:

  • 确实如此。我将尝试使用不同的方法来解决这个问题。我有一种感觉,这段代码只会变得更丑。
  • 同意。可以通过一点 SFINAE 魔法使其按原样工作,但如果你不小心,这也会导致疯狂,例如 serial &lt;&lt; liquid_crystal::move{ 1, 2 }; 有效。我正在想办法解决这个问题,但我能想到的一切都涉及在子类中定义 operator&lt;&lt;() (渲染 SFINAE 没有实际意义)或需要在运行时确定 *this 的动态类型(这通常表明代码有问题)。
【解决方案2】:

最佳解决方案是让每个类自行定义operator&lt;&lt;。但是,如果无法做到这一点,则可以滥用 SFINAE 和动态转换来达到预期的效果。

我确实建议在您的最终产品中使用它。我只是在这里发布它,希望比我更有知识的人可以找到一种方法来使这项工作在没有 dynamic_cast 的情况下工作,而不会引入可能导致未定义行为的代码。

#include <iostream>
#include <type_traits>

// -----

// void_t definition.  If your compiler has provisional C++17 support, this may already be
//  available.
template<typename...>
struct make_void { using type = void; };

template<typename... T>
using void_t = typename make_void<T...>::type;

// -----

// SFINAE condition: Is argument a stream command?

class stream;

template<typename T, typename = void>
struct is_stream_command : std::false_type
{};

template<typename T>
struct is_stream_command<T, void_t<typename T::command_for>> :
    std::integral_constant<bool, std::is_base_of<stream, typename T::command_for>::value ||
                                 std::is_same<stream, typename T::command_for>::value>
{};

// -----

class stream
{
public:
    // Stream command type.  Used as parent class for actual commands.
    // Necessary for version of print() that handles stream commands.
    // Not needed for SFINAE.
    // If used, each command will need a constructor.
    struct command
    {
        using command_for = stream;
        // This typedef should be defined in each command, as the containing class.
        // If command is valid for multiple classes, this should be their parent class.
        // Used for SFINAE.

        virtual ~command() = default;
    };

    struct fill : command
    {
        int n;
        char ch;
        using command_for = stream;

        fill(int n, char ch) : n(n), ch(ch) {}
    };

    stream()
    {}

    virtual ~stream()
    {}

    // Called for stream commands.  Solves the issue you were having, but introduces a
    //  different issue: It casts "this", which can cause problems.
    // Tied with a version of stream::print() that handles commands.
    template<typename T>
    typename std::enable_if<is_stream_command<T>::value, stream&>::type
    operator<<(T t)
    {
        static_cast<typename T::command_for*>(this)->print((stream::command*) &t);
        return *this;
    }

    // Called for other output.
    template<typename T>
    typename std::enable_if<!is_stream_command<T>::value, stream&>::type
    operator<<(T t)
    {
        this->print(t);
        return *this;
    }

protected:
    virtual void print(char) = 0;

    virtual void print(stream::command* com);

    template <typename T>
    void print(T);
};

template <>
void stream::print<const char*>(const char* str)
{
    while (*str != '\0')
        this->print(*(str++));

    std::cout << std::endl; // For testing.
}

template <>
void stream::print<stream::fill>(stream::fill f)
{
    std::cout << "fill "; // For testing.

    while (f.n > 0)
    {
        this->print(f.ch);
        f.n--;
    }

    std::cout << std::endl; // For testing.
}

// Version of print() which handles stream commands.
// Solves problem introduced by operator<<() for commands, but introduces its own problem:
//  dynamic casting.
void stream::print(stream::command* com) {
    if (dynamic_cast<stream::fill*>(com)) {
        std::cout << "Valid command: "; // For testing.
        this->print(*(dynamic_cast<stream::fill*>(com)));
    } else {
        // Handle as appropriate.
        std::cout << "Invalid command." << std::endl;
    }
}

// -----

class serial_port : public stream
{
public:
    struct set_baudrate : stream::command
    {
        int baud;
        using command_for = serial_port;

        set_baudrate(int baud) : baud(baud) {}
    };

    using stream::stream;

private:
    void print(char c) override
    {
        // TODO: print to the actual serial port
        std::cout << c;
    }
    void print(stream::command* com) override;

    template <typename T>
    void print(T t)
    {
        stream::print<T>(t);
    }

    // Necessary to allow stream::operator<<() to call private member function print().
    template<typename T>
    friend typename std::enable_if<is_stream_command<T>::value, stream&>::type
    stream::operator<<(T t);
};

template <>
void serial_port::print<serial_port::set_baudrate>(serial_port::set_baudrate)
{
    this->print("set_baudrate");
}

void serial_port::print(stream::command* com) {
    if (dynamic_cast<serial_port::set_baudrate*>(com)) {
        std::cout << "Valid command: "; // For testing.
        this->print(*(dynamic_cast<serial_port::set_baudrate*>(com)));
    } else {
        // Invalid commands fall through to parent class, in case they're valid for any
        //  stream.
        this->stream::print(com);
    }
}

// -----

class liquid_crystal : public stream
{
public:
    struct move : stream::command
    {
        int x;
        int y;
        using command_for = liquid_crystal;

        move(int x, int y) : x(x), y(y) {}
    };

    using stream::stream;

private:
    void print(char c) override
    {
        // TODO: print to a character LCD
        std::cout << c;
    }
    void print(stream::command* com) override;

    template <typename T>
    void print(T t)
    {
        stream::print<T>(t);
    }


    // Necessary to allow stream::operator<<() to call private member function print().
    template<typename T>
    friend typename std::enable_if<is_stream_command<T>::value, stream&>::type
    stream::operator<<(T t);
};

template <>
void liquid_crystal::print<liquid_crystal::move>(liquid_crystal::move)
{
    this->print("move");
}

void liquid_crystal::print(stream::command* com) {
    if (dynamic_cast<liquid_crystal::move*>(com)) {
        std::cout << "Valid command: "; // For testing.
        this->print(*(dynamic_cast<liquid_crystal::move*>(com)));
    } else {
        // Invalid commands fall through to parent class, in case they're valid for any
        //  stream.
        this->stream::print(com);
    }
}

// -----

int main()
{
    liquid_crystal lcd;
    lcd << 'a' << " " << liquid_crystal::move{ 1, 2 };
    serial_port serial;
    serial << 'a' << " " << serial_port::set_baudrate{ 9600 };

    std::cout << "Are they valid commands?" << std::endl;
    std::cout << std::boolalpha;
    std::cout << "stream::fill, for serial_port: ";
    serial << stream::fill{ 3, 'a' };
    std::cout << "stream::fill, for liquid_crystal: ";
    lcd << stream::fill{ 3, 'a' };

    std::cout << "serial_port::set_baudrate, for serial_port: ";
    serial << serial_port::set_baudrate{ 9600 };
    std::cout << "serial_port::set_baudrate, for liquid_crystal: ";
    lcd << serial_port::set_baudrate{ 9600 };

    std::cout << "liquid_crystal::move, for serial_port: ";
    serial << liquid_crystal::move{ 1, 2 };
    std::cout << "liquid_crystal::move, for liquid_crystal: ";
    lcd << liquid_crystal::move{ 1, 2 };
}

这将有以下输出:

a b
Valid command: move
a b
Valid command: set_baudrate
Are they valid commands?
stream::fill, for serial_port: Valid command: fill aaa
stream::fill, for liquid_crystal: Valid command: fill aaa
serial_port::set_baudrate, for serial_port: Valid command: set_baudrate
serial_port::set_baudrate, for liquid_crystal: Invalid command.
liquid_crystal::move, for serial_port: Invalid command.
liquid_crystal::move, for liquid_crystal: Valid command: move

我不喜欢这个解决方案,因为它依赖于dynamic_cast。但是,我发现有缺陷的解决方案通常比没有解决方案要好,因为它可以帮助您找到好的解决方案。这解决了链接问题,并且可以理想地用作不需要任何强制转换的解决方案的基础。

【讨论】:

    猜你喜欢
    • 2016-09-26
    • 1970-01-01
    • 2010-12-28
    • 2012-04-03
    • 1970-01-01
    • 1970-01-01
    • 2010-11-10
    相关资源
    最近更新 更多