善意提醒

如果您打开本站很慢,布局排版混乱,并且看不到图片,那么可能是因为您还没有掌握用科学的方法上网的本领。
显示标签为“CPP”的博文。显示所有博文
显示标签为“CPP”的博文。显示所有博文

2017-03-29

对 VS2013 下 C++11 的精准转发与通用引用的一点研究

在 C++11 中,允许用以下方式编写模板函数:
#include <iostream>
#include <string>
class A
{
public:
    template <typename T>
    void foo(T&& t)
    {
        T _t = std::forward<T>(t);
    }
};
int main()
{
    std::string s1 = "test";
    A a;
    a.foo(s1);
    std::cout << "1:" << s1 << std::endl;
    a.foo(std::move(s1));
    std::cout << "2:" << s1 << std::endl;
    a.foo("ok");
}
以上代码在 Visual Studio 2013 上测试通过。输出是:
1:test
2:
可以看到,模板函数 A.foo 只有一个声明和实现,但既可以接受左值,也可以接受右值。并且当 S1 被当作右值引用传入的时候,其值是确确实实被「丢弃」了。这就是所谓的精准转发(把 T 的类型准确地传递到使用者),以及通用引用(用一个 T&& 就可以表示所有的情况)。对于要写库的程序员来说,可谓是一个福音了。
然而,对于模板类,下面的写法看上去很好,但是编译会报错的:
template <typename T>
class A
{
    T _t;
    public:
    void foo(T&& t)
    {
        _t = std::forward<T>(t);
    }
};
int main()
{
    std::string s1 = "test";
    A<std::string> a;
    a.foo(s1);
    std::cout << "1:" << s1 << std::endl;
    a.foo(std::move(s1));
    std::cout << "2:" << s1 << std::endl;
    a.foo("ok");
}
编译后报错:
error C2664: “void A<std::string>::foo(T&&)”: 无法将参数 1 从“std::string”转换为“std::string&&”
with
[
    T=std::string
]
无法将左值绑定到右值引用
这大概是因为,T 的类型在 A<std::string> 的时候就确定了,因此编译器无法进行更多的类型推导。
那么怎么办呢?其实也不难,foo  函数像下面这样写就可以了:
template <typename T>
class A
{
    T _t;
    public:
    template <typename X>
    void foo(X&& t)
    {
        _t = std::forward<X>(t);
    }
};
在函数模板中用一个新类型就可以了。如果 T 跟 X 不一致,那么编译器反正会检查出来的。不用担心。
还有一个问题:有的时候我们会把 foo 的实现写在 class 的外面。那这个时候怎么办呢?
我本来想抛题目给大家去做。不过都到最后了卖关子也没什么意思,还是直说吧:
template <typename T>
class A
{
    T _t;
    public:
    template <typename X>
    void foo(X&& t);
};
template <typename T>
template <typename X>
void A<T>::foo(X&& t)
{
    _t = std::forward<X>(t);
}
标红的两行,只能是这个顺序。类的模板定义在上面,函数的模板定义在下面。颠倒过来,报错。要写在一行也可以,先左后右就行。但是要想把尖括号打开强行并成一句,报错。

2011-10-24

VC 和 BCB 那点事——DLL 导出函数的结构体参数

还是那个项目,VC 写一个 DLL,导出若干 C 函数,BCB 来调用。实际情况比这个复杂,不过与我这次要讲的这个问题无关,所以在此省略了。

在解决了上次遇到的虚函数表顺序问题后,继续往下调试,又碰到了一个很怪的问题。有一个导出函数,一调用便崩溃。进去一看吧,崩溃点的代码别的导出函数也在用,没什么不对。虽然改改代码,可以做到不抛出异常,但该导出函数返回的值又不正常。反正它就是干不了想干的活。
该函数本身很简单,因此怀疑不是内部逻辑导致的问题。通过对比,发现该函数与其它工作正常的导出函数有一个明显的区别:它返回了一个自定义的结构体作为返回值。函数的声明大概是这样子的:
LONDATEEX __stdcall GetDataDate( USHORT sMarket);
由于别的可能性被一一排除,因此焦点慢慢移到这个情况上来。查了查网上的信息,发现有一篇文章(被墙,由此可见 GFW 的反动性质)提到了跨模块调用时的内存管理问题,并据此总结了几条规则。其中一条,便是不要在跨模块函数调用中使用类或结构体作为参数或返回值类型。

该文章在这一点上略有阐述:因为内存管理模式不一样,所以类和结构体这种可能会进行内存分配、回收的数据类型,一旦用作跨模块调用的参数或返回值,就会导致不确定的后果。我认为这个解释是合理的。我之前也已经想到了这一点,不过接下来还有。
该文章还提出了改进建议——如果一定要作为参数或返回值传递,那么应该采用所谓的纯结构体,即只包含了简单数据类型的结构体,构造、析构时不涉及内存分配和释放。该文认为,这种纯结构体的数据类型,可以用于 DLL 导出函数的参数或返回值。碰巧了,我这次这个结构体,就是个纯结构体。那么按照这篇文章的理论,不应该有问题才对。

不弄明白这个问题,始终是不甘心,于是我动手做实验了。先用 VC 写了一个很简单的 DLL,声明了一个最简单的结构体,包含两个 int 类型的变量。然后用 BCB 来调用。并且,在做这个实验的时候,我特地注意避免了字节对齐不一致的问题。
嗯,结果是什么呢?调用完成了,可无论传进去的参数值还是传出来的返回值,都不对。同样的函数声明方式,都用了 __stdcall 的另一个只包含简单数据类型作为参数和返回值的 DLL 导出函数,工作正常。

两相对照,显然,就算是所谓纯结构体,也不见得能够放心地用于跨模块的函数调用。至少,在 VC 和 BCB5 之间,是会有问题的。别人说的,不一定就是对的,至少不一定全对。尽信书不如无书。

解决方案也很简单,指针是一个简单数据类型,把结构体的指针作为参数传进去就行了。比如:
BOOL __stdcall GetDataDate( USHORT sMarket, LONDATEEX* pDate);
还有别的办法也行,比如利用 COM,或者通过操作系统级别管理的对象来传递数据。总之别让两种编译器编译出来的东西去分别猜对方是怎么管理内存的就行。

2011-10-21

VC 和 BCB 那点事——虚函数的顺序问题

因为工作需要,编写了一个 C++ 类,供一个 VC 下的 DLL 使用。用法类似回调对象,将 C++ 对象的指针传给 DLL 导出的函数。类里面要被用到的函数全是虚函数,父类是个彻底的抽象类,由 DLL 的开发人员提供 .h 头文件。VC 和 BCB 在虚函数表的实现上是一致的,因此可以跨模块调用。

大部分虚函数的调用测试都很顺利,但在一个函数上遇到了麻烦。不,应该说是两个。在测试过程中发现,当 DLL 想要调用我提供的 C++ 类的 FunA 函数时,总是错误地调用到了 FunB 函数。反过来也一样,结果就好像是 DLL 把 A 函数和 B 函数搞反了。
class A
{
    ……
    virtual FunA() = 0;
    virtual FunB() = 0;
    ……
};
既然是按照虚函数表来调用,那就跟函数名无关。这一点很快就得到了证实。现在唯一剩下的就是顺序问题。根据资料,虚函数表里面的顺序是根据函数的声明顺序来排布的,也就是说 .h 头文件决定了顺序。调换了一下顺序,果然就正常了,可为什么会反呢?
其实不能只看这么一点儿代码,这个问题和上下文有关:
class A
{
    ……
    virtual FunB(int J) = 0;
    virtual FunA() = 0;
    virtual FunB() = 0;
    ……
};
最后是 CSDN 上一篇博文揭示了这个问题:VC 会把重载函数给排在一起,不管中间有没有插队者。参见:http://blog.csdn.net/doudouhuy/article/details/4348531
也就是说,最终的顺序实际上是:
class A
{
    ……
    virtual FunB(int J) = 0;
    virtual FunB() = 0;
    virtual FunA() = 0;
    ……
};
而 BCB 大概不会做这个改动,因此调用到的函数就和预期的不一样了。

最后说一下。为了避免遇到类似问题,建议在进行跨模块的虚函数调用的时候,彻底避开重载函数出现的情况。把所有函数的名称都声明得不同,就不会轮到编译器来干扰了。

2011-10-04

初始化,真的很重要

刚花了大半天的时间解决掉一个 Bug,再次证明了变量初始化的重要性。照例,还是先来看一小段代码:

map<string, TEOBJ> telist; // 全局对象
……
TEOBJ spte(3);
telist["debug"] = spte;

很简单吧?按道理这点代码应该没有什么问题。不过在最后一句中却抛出了异常,发生了崩溃。负责这部分代码的人昨天就请了假回家了,节后还有婚假,一走要近二十天,只好自己动手找原因。

这里的 TEOBJ,是一个自己定义的类。在我的这个案例中,它是一个底层库里面定义的数据对象类,用在了很多地方。基于这个前提,我首先判断这个 Bug 的故障点在赋值号的左边。

可是左边也是个 std::map 的相当标准的用法,STL 出错的可能性恐怕比我们自己的底层库更低,而且这个用法我以前也用过很多,没有出过什么问题。怀疑的眼光于是又回到了赋值号的右边。

那么到底是哪一边呢?我也拿不准了,于是排除法上马。通过几轮替换,我终于把问题基本定性为「由 TEOBJ 引起,但需要在赋值到 map 的 [] 运算符时产生」。

既然说到赋值,那么就该看看 TEOBJ 的相关声明了:

class TEOBJ {
public:
    int GroupID;
private:
    int GroupCode;
public:
    TEOBJ::TEOBJ() {};
    TEOBJ::TEOBJ(int nGroupID) {
        GroupID = nGroupID;
        GroupCode = TDataStore::Groups[GroupID]->Code;
    }
    TEOBJ::TEOBJ(const TEOBJ& r) {
        GroupID = r.GroupID;
        GroupCode = TDataStore::Groups[GroupID]->Code;
    }
    const TEOBJ& TEOBJ::operator=(const TEOBJ& r) {
        GroupID = r.GroupID;
        GroupCode = TDataStore::Groups[GroupID]->Code;
    }
};

这个 TEOBJ 重载了拷贝构造函数和赋值运算符,在其中通过查询一个数据字典确保 GroupCode 有准确的取值。

首先,赋值运算符重载这里的返回值有点小问题。const 引用类型的返回值与习惯上的 non-const 引用类型的返回值有所区别。不过这个最多造成编译时语法检查上的问题,不至于引起指针错误。

其次,就是这个 TDataStore::Groups[GroupID] 的数组的使用值得怀疑了。

实际案例比这个情况要复杂一些,而且我对于 BCB 的使用还不算熟悉,调试基本靠 OutputDebugString。问题就诡异在,往 TEOBJ::operator=() 里面只要任意加一行 OutputDebugString,故障就消失了。搞得好像是野指针一样。

我在此浪费了很多时间,走了不少弯路。后来,一咬牙用上了 windbg,配合 map2dbg,好歹是得到了 CallStack。但是 map2dbg 之后得到的符号名称又和我以前看到的有些不同,于是又错判了位置。

最后,我终于在 windbg + 排除法的协助下找到了问题。问题根本不在赋值运算符重载函数中。尽管这里代码中明显是用到了它,但它并没有出问题。抛出异常的是拷贝构造函数。这个就要从 map::operator[] 的实现说起了。

Borland C++ Builder 5 里面的 std::map,其实现代码在 map.h 里面,是一个 inline 函数。挖出来一看,倒也很简单:

mapped_type& operator[] (const key_type& k) {
    value_type tmp(k,T());
    return (*((insert(tmp)).first)).second;
}

可以看到,这里先生成了一个临时变量(其实本例中就是 pair<string, TEOBJ>)。在生成这个临时变量的时候,采用了无参数的 TEOBJ 构造函数。此时 TEOBJ::GroupID 还是没有初始化的,在 Release 版本中它可能是任意值(TEOBJ 所在的底层库就是 Release 编译)。

实际使用时,TEOBJ 都通过 TEOBJ::TEOBJ(int nGroupID) 来初始化,因此不会产生这种未被正确初始化的实例。但在 map::operator[] 中,由于是「先插入空值,再传出引用用于赋值」,这个时候就使得拷贝构造函数被调用,于是产生了类似的访问野指针的效果,导致故障的出现。

通过在拷贝构造函数中用 OutputDebugString 输出 GroupID 的取值,确认了这一点。

至此,真相大白。总结经验教训如下:

  1. 变量的初始化很重要,一个也不能放松,不能因为眼下不初始化也不会出问题,就放松警惕。做底层库的人尤其应该重视——你的代码不只是你一个人在用!
  2. 数组的下标,或者说指针的偏移量,这种变量与指针基本上是同样的性质。要提防野指针,它们也要算上。
  3. 调试手段无所谓牛刀不牛刀,好用就用,不要因为感觉问题小、简单,就先用笨办法尝试,直到一筹莫展了之后才考虑别的办法。如果我一开始就用 windbg,时间上可能会节省更多一些。

2007-04-16

OTL建立连接时可能会遇到的一个bug

最近因为工作原因,从 OCICPP 改为用 OTL 做 Oracle 开发。初时挺诧异的,怎么只有 .h 没有 .cpp?看过才知道原来一个头文件就全做完了。它是对 OCI 的一个封装,可以用 stream 的方式去操作数据库。用起来还是比较简便,但是感觉封装得多了点。虽然 OCI 那些函数也挺复杂的,但至少觉得一切都在自己掌握之中。OTL 这么一封装了之后,有一些实现细节就不得而知了,这就导致了今天发现的一个 bug。

根据 OTL 关于 otl_connect 的说明,其构造函数之一是可以直接用来连接数据库的。虽然正儿八经做这个事情的是 rlogon(db_string),但是对构造函数 otl_connect(connect_str, ...) 的说明是其等于 otl_connect(void) 再加上一个 rlogon(connect_str)。所以,一般就这样写了:

otl_connect* pdb = new otl_connect("...");

然后把 pdb 存在自己的数据库连接池中。如果连接失败,那么会抛出一个 otl_exception 异常。

可是,今天却遇到意外。连接字符串中,tnsname 写对了,但用户名 / 密码没对。于是测试的时候发现,多次连接之后,数据库那边内存爆掉了,ORACLE.EXE 的线程数也多到疯掉。别的客户端全连接不上了,报 ORA-00020 错误。
process 数量多,这倒也在情理之中。可是看了看 session,发现会话其实很少。猜测是什么东西没有释放掉,不是 connect 就是 cursor。查了一下代码,发现没有什么大问题,connect 都是连接池管理,不会无限多下去的。otl_stream 也都是在栈中声明,花括号之后就自动析构了,cursor 也不应该是问题。郁闷中,发现正常的连接反而不会有泄漏发生,会泄漏的都是连接错误的时候。但是连 tnsname 都不对反而就不会了,估计是因为 OCI 那边根本就无法建立一个连接,也就耗不了资源。这下定位到了,就是这个创建连接的代码上有问题。

这段创建连接的代码是这样写的。注意其中捕捉异常的部分:

otl_connect* pdb = NULL;
try
{
  pdb = new otl_connect("...");
}
catch(otl_exception& e)
{
  ……
  if (NULL != pdb)
    delete pdb;
  pdb = NULL;
}

可以看到,如果连接不成功时没有生成对象,那么应该返回空指针,这样 delete 指令也不会发生。如果有对象生成,那么在异常处理代码中应该已经把这个对象给销毁了,而对象占用的资源应该也已经释放了。但事实就是不同,由于 OTL 在这个构造函数中封装了实现细节,而显然这个实现并不完美,于是有了泄漏。

采用如下的代码,便不存在这个问题了:

otl_connect* pdb = NULL;
try
{
  pdb = new otl_connect();
  pdb->rlogon("...");
}
catch(otl_exception& e)
{
  ……
  if (NULL != pdb)
    delete pdb;
  pdb = NULL;
}

根据 OTL 的说明,这两种建立连接的方法应该没有区别。但事实上 OTL 提供的范例代码中都是采用后一种方法。

追进 OTL 的头文件中去看,应该可以弄明白原委。不过为了完成任务,没有那么多时间了,这个任务只好留到下次再说。大致估计了一下,应该是因为在 new otl_connect(connect_str) 的时候就抛了异常,于是代码跳转到了 catch 处,pdb 根本没得到对象指针的赋值,这样新生成的对象就丢了。建议用 OTL 的各位,在栈中是无所谓啦,但如果要在堆中初始化一个连接,千万不要图省事想用构造函数直接一步到位哦!

后记:
其实,如果构造函数中抛了异常,而对象中存在着自己管理的资源,那么很可能会发生资源泄漏,这对于 C++ 程序员而言几乎可以作为一条准则了。在我后来看到《Effective C++》之后,就知道本文属于其中案例之一。我认为,OTL 就不应该支持在构造函数中进行连接这种方式。你给程序员两条路可走,那每条路都肯定会有人走。如果其中一条有坑,那这条路就没必要对外开放了。