代码改变世界

从operator<<谈函数重载决议

2012-12-10 10:25  yu_yu  阅读(760)  评论(2编辑  收藏  举报

一. 背景

项目中需要对数据库查询访问的业务,在写数据库sql语句代码时,由于没有特别复杂的格式化需求,决定采用C++标准库中的stream来进行sql语句的格式化。有两点好处:

1). 类型安全

2). 使用方便

比如

std::ostringstream os;
std::uint32_t id = 10;
os << "select * from user_t where id = " << id;

sql_excute(os.str());

由于数据库对输入的字符串参数需要加上单引号,导致在格式化字符串参数的时候需要额外调用一个增加单引号的函数,如下:

std::string add_quote(const std::string &param) { return "'" + param + "'"; } void print() { std::ostringstream os; std::uint32_t id = 10; std::string name = "test_name";

os << "select * from user_t where id = " << id << " and " << "name = " << add_quote(name); sql_excute(os.str()); }

这样是可以解决问题,但是使用起来非常繁琐,特别是遇到有很多字符串参数的时候,要额外增加很多add_quote的调用,单单从实用角度来讲就特别不方便,很有可能会漏掉一些字符串参数。于是,我就希望能通过某种技术手段,来规避掉这个问题(C++程序员最大的毛病就是喜欢去创造轮子而不务正业)。

二. 需求

1). 对operator<<(std::ostringstream &, const std::string &msg)中,对传入的msg自动加上单引号

2). 对其他类型的重载保持原有语义

三. 解决方案

基于以上两点,仅仅需要重载掉operator<<即可,各位看官请看

struct sql_ostream
    : std::wostringstream
{};

sql_ostream &operator<<(sql_ostream &os, const std::wstring &msg)
{
    std::wostringstream &tmp = os;
    tmp << L"'" << msg << L"'";
    return os;
}

template < typename T >
sql_ostream &operator<<(sql_ostream &os, T msg)
{
    std::wostringstream &tmp = os;
    tmp << msg;
    return os;
}

对于std::wstring做特殊处理,其他类型的参数则调用基类进行默认处理。看上去一切正常,是真的吗?未必!

当我们遇到额外的需求时,就会失效了,来看看这个例子:

struct AA
{
    template < typename CharT >
    operator std::basic_string<CharT>()
    {
        return std::basic_string<CharT>();
    }
};

os << AA();

在这里,我们的AA提供隐式转换(别骂我脑残干嘛要用这招,其实也有好处的)到basic_string。哈哈,现在不行了吧,编译期报错了吧。由于在此次隐式转换中,需要根据接受字符串对象是char还是wchar_t而是一个模版参数,所以,os << AA()并不会选择operator<<(sql_ostream &os, const std::wstring &),而是选择了接受模版参数的第二个重载中,为什么呢?

四. 函数重载的定义

这里引出了一个ADL(Koenig)查找。

是指在编译器对无限定域的函数调用进行名字查找时,所应用的一种查找规则。

首先来看一个函数所在的域的分类:

1 :类域(函数作为某个类的成员函数(静态或非静态))

2 :名字空间域

3 :全局域

而 Koenig 查找,它的规则就是当编译器对无限定域的函数调用进行名字查找时,除了当前名字空间域以外,也会把函数参数类型所处的名字空间加入查找的范围。

ADL 就是为了确保使用类型 X 的对象 x 时能够像使用 X 的成员函数一样简单 (ensure that code that uses an object x of type X can use its nonmember function interface as easily as it can use member functions)

这里是wiki上的解释,这里是Heber Surte的一个例子,而这里是一篇CSDN的翻译。

在我们这里,因为涉及到模版函数匹配的问题,又引入了一个SFINAE原则(匹配错误不算失败)。当然,有两个准则需要记住:

1. 函数模板特化并不参与重载决议。只有在某个主模板被重载决议选中的前提下,其特化版本才有可能被使用。而且,编译器在选择主模板的时候并不关心它是否有某个特化版本

2. 如果一个普通的非模板函数跟一个函数模板在重载解析的参数匹配中表现一样好的话,编译器会选择普通函数

这里是wiki对SFINAE的解释。

 

五. 再次尝试

好了,知道原因了,来看看如何解决掉这个问题。为了能在编译期决策,我们需要引入一个间接层和一个编译期选择重载的模版组件std::enable_if。关于std::enable_if的文章,可以看这里这里这里。赶紧来看看我们的代码如何处理

namespace stdex
{
    typedef std::wostringstream tOstringstream;
    typedef std::wstring tString;
}

struct sql_ostream
{
    stdex::tOstringstream os_;

    stdex::tString str() const
    {
        return std::move(os_.str());
    }


    struct serialize_impl
    {
        template < typename T >
        static sql_ostream &to(sql_ostream &os, T msg, 
            typename std::enable_if<
                std::is_arithmetic<T>::value ||
                std::is_pointer<T>::value 
            >::type *N = 0)
        {
            static_assert(std::is_arithmetic<T>::value || std::is_pointer<T>::value,
                "must a arithmetic or pointer type");

            os.os_ << msg;
            return os;
        }

        static sql_ostream &to(sql_ostream &os, const stdex::tString &msg)
        {
            os.os_ << _T("'") << msg << _T("'");
            return os;
        }
    };


    template < typename T >
    sql_ostream &serialize(T &&msg)
    {
        return serialize_impl::to(*this, msg);
    }

};

template < typename T >
sql_ostream &operator<<(sql_ostream &os, T &&msg)
{
    os.serialize(msg);
    return os;
}

这里有几点需要注意:

1). 并没有采用继承自std::stringstream

2.) template < typename T > sql_ostream &operator<<(sql_ostream &os, T &&msg) 一个暴露对外的接口,参数传递采用的是&&

3). serialize_impl中重载了to,第一个to函数的第三个参数是根据std::enable_if来推断的

对于第1点,继承是条贼船,上去了就下不来

对于第2点,使接口简单,这里只是起到一个完美转发的作用,可以右值引用

对于第3点,看过前面给出的链接,大家应该都知道enable_if的作用,就是为了消除掉模版类或者模版函数在重载时的二义性

这样,就可以使AA可以完美的通过隐式转换进入到to(sql_ostream &os, const stdex::tString &msg) 中。

六. 总结

C++的重载决议是个很复杂的过程,特别是用在模版函数或者模版类的情况下更复杂,为了让编译器知道更多地类型信息,我们引入了std::enable_if,对于重载就会稍微简单点。