Observations on TDD in C++ (long) 56

Posted by Dean Wampler Wed, 04 Jul 2007 04:15:09 GMT

I spent all of June mentoring teams on TDD in C++ with some Java. While C++ was my language of choice through most of the 90’s, I think far too many teams are using it today when there are better options for their particular needs.

During the month, I took notes on all the ways that C++ development is less productive than development in languages like Java, particular if you try to practice TDD. I’m not trying to start a language flame war. There are times when C++ is the appropriate tool, as we’ll see.

Most of the points below have been discussed before, but it is useful to list them in one place and to highlight a few particular observations.

Based on my observations last month, as well as previously experience, I’ve come to the conclusion that TDD in C++ is about an order of magnitude slower than TDD in Java. Mostly, this is due to poor or non-existent tool support for automated refactorings, no error detection as you type, and the requirement to compile and link an executable test.

So, here is my list of impediments that I encountered last month. I’ll mostly use Java as the comparison language, but the arguments are more or less the same for C# and the popular dynamic languages, like Ruby, Python, and Smalltalk. Note that the dynamic languages tend to have less complete tool support, but they make up for it in other ways (off-topic for this blog).

Getting Started

There is more setup effort involved in configuring your build environment to use your chosen unit testing framework (e.g., CppUnit) and to create small executables, one each for a single or a few tests. Creating many small tests, rather than one big test (e.g., a variant of the actual application). This is important to minimize the TDD cycle.

Fortunately, this setup is a one-time “charge”. The harder part, if you have legacy code, is refactoring it to break hard dependencies so you can write unit tests. This is true for legacy code in any language, of course.

Complex Syntax

C++ has a very complex syntax. This makes it hard to parse, limiting the capabilities of automated tools and slowing build times (more below).

The syntax also makes it harder to program in the language and not just for novices. Even for experts, the visual noise of pointer and reference syntax obscures the story the code is trying to tell. That is, C++ code is inherently less clean than code in most other languages in widespread use.

Also, the need for the developer to remember whether each variable is a pointer, a reference, or a “value”, and how to manage its life-cycle, requires mental effort that could be applied to the logic of the code instead.

Obsolete Tool Support

No editor or IDE supports non-trivial, automated refactorings. (Some do simple refactorings like “rename”.) This means you have to resort to tedious, slow, and error-prone manual refactorings. Extract Method is made worse by the fact that you usually have to edit two files, an implementation and a header file.

There are no widely-used tools that provide on-the-fly parsing and error indications. This alone increases the time between typing an error and learning about it by an order of magnitude. Since a build is usually required, you tend to type a lot between builds, thereby learning about many errors at once. Working through them takes time. (There may be some commercial tools with limited support for on-the-fly parsing, but they are not widely used.)

Similarly, none of the common development tools support incremental loading of object code that could be used for faster unit testing and hence a faster TDD cycle. Most teams just build executables. Even when they structure the build process to generate small, focused executables for unit tests, the TDD cycle times remain much longer than for Java.

Finally, while there is at least one mocking framework available for C++, it is much harder to use than comparable frameworks in newer languages.

Manual Memory Management

We all know that manual memory management leads to time spent finding and fixing memory errors and leaks. Avoiding these problems in the first place also consumes a lot of thought and design effort. In Java, you just spend far less time thinking about “who owns this object and is therefore responsible for managing its life-cycle”.

Dependency Management

Intelligent handling of include directives is entirely up to the developer. We have all used the following “guard” idiom:

    #ifndef MY_CLASS_H
    #define MY_CLASS_H
    ...
    #endif

Unfortunately, this isn’t good enough. The file will still get opened and read in its entirety every time it is included. You could also put the guard directives around the include statement:

    #ifndef MY_CLASS_H
    #include "myclass.h"
    #endif

This is tedious and few people do it, but it does avoid the wasted file I/O.

Finally, too few people simply declare a required class with no body:

    class MyClass;

This is sufficient when one header references another class as a pointer or reference. In our experience with clients, we have often seen build times improve significantly when teams cleaned up their header file usage and dependencies, in general. Still, why is all this necessary in the 21st century?

This problem is made worse by the unfortunate inclusion of private and protected declarations in the same header file included by clients of the class. This creates phantom dependencies from the clients to class details that they can’t access directly.

Other Debugging Issues

Limited or non-existent context information when an exception is thrown makes the origin of the exception harder to find. To fill the gap, you tend to spend more time adding this information manually through logging statements in catch blocks, etc.

The std::exception class doesn’t appear to have a std::string or const char* argument in a constructor for a message. You could just throw a string, but that precludes using an exception class with a meaningful name.

Compiler error messages are hard to read and often misleading. In part this is due to the complexity of the syntax and the parsing problem mentioned previously. Errors involving template usage are particular hard to debug.

Reflection and Metaprogramming

Many of the productivity gains from using dynamic languages and (to a lesser extent) Java and C# are due to their reflection and metaprogramming facilities. C++ relies more on template metaprogramming, rather than APIs or other built-in language features that are easier to use and more full-featured. Preprocessor hacks are also used frequently. Better reflection and metaprogramming support would permit more robust proxy or aspect solutions to be used. (However, to be fair, sometimes a preprocessor hack has the virtue of being “the simplest thing that could possibly work.”)

Library Issues

Speaking of std::string and char*, it is hard to avoid writing two versions of methods, one which takes const std::string& arguments and one which takes const char* arguments. It doesn’t matter that one method can usually delegate to the other one; this is wasted effort.

Discussion

So, C++ makes it hard for me to work the way that I want to work today, which is test-driven, creating clean code that works. That’s why I rarely choose it for a project.

However, to be fair, there are legitimate reasons for almost all of the perceived “deficiencies” listed above. C++ emphasizes performance and backwards-compatibility with C over all other considerations. However, they come at the expense of other interests, like effective TDD.

It is a good thing that we have languages that were designed with performance as the top design goal, because there are circumstances where performance is the number one requirement. However, most teams that use C++ as their primary language are making an optimal choice for, say, 10% of their code, but which is suboptimal the other 90%. Your numbers will vary; I picked 10% vs. 90% based on the fact that performance bottlenecks are usually localized and they should be found by actual measurements, not guesses!

Workarounds

If it’s true that TDD is an order of magnitude slower for C++ then what do we do? No doubt really good C++ developers have optimized their processes as best as they can, but in the end, you will just have to live with longer TDD cycles. Instead of write just enough test to fail, make it pass, refactor, it will be more like write a complete test, write the implementation, build it, fix the compilation errors, run it, fix the logic errors to make the test pass, and then refactor.

A Real Resolution?

You could consider switching to the D language, which is link compatible with C and appears to avoid many of the problems described above.

There is another way out of the dilemma of needing optimal performance some of the time and optimal productivity the rest of the time; use more than one language. I’ll discuss this idea in my next blog.

Trackbacks

Use the following link to trackback from your own site:
http://blog.objectmentor.com/articles/trackback/8785

Comments

Leave a response

  1. Avatar
    Przemyslaw Owczarek about 7 hours later:

    Good analysis. I have one remark regarding IDEs. Try latest SlickEdit. It has some re factorings that works pretty well (even extract class). However it works ok only if you can setup g++ for your project. I use sun compilers so it’s not a solution for me :(

  2. Avatar
    Kristian Dupont about 8 hours later:

    I agree that C++ is poorly suited for TDD but I don’t agree with all your points. To me, interfaces are the thing I miss the most. Not having interfaces makes dependency injection difficult. You can do it with abstract base classes but then you have to make your functions virtual which is not in the spirit of the language. Or you can do it with templates but this introduces quite a bit of complexity. Regarding manual memory management, I think this is more of a religious debate. When I code C#, I find that my dependencies tend to form a non-directed graph whereas my C++ code forms a tree structure. It’s a little more rigid but in my experience it scales a little bit better (I more or less the same feeling about static typing compared to dynamic typing). I am aware that a lot of people disagree with me on this. As for include guards, modern compilers take care of that for you. No need for those any more. And I never, ever write a method that accepts char* instead of string..

  3. Avatar
    Przemyslaw Owczarek about 10 hours later:

    Kristian, What problem do you find with using abstract base classes with pure virtual methods? For me it’s an interface and whever it’s in the spirit of the language or not is a religious debate :)

  4. Avatar
    Dean Wampler about 11 hours later:

    Przemyslaw, I wasn’t aware that SlickEdit did this for g++. Some of the developers I worked with used SlickEdit and probably didn’t even know this feature existed. I certainly didn’t.

    Concerning your particular problem, could you create a development build (e.g., for your unit tests) that uses g++ instead of the Sun compilers? That way you could have your refactoring when you need it.

    Kristian, thanks for the observation about memory management. I can see how attention to it can lead to a better object graph. It’s an interesting tradeoff; a little more manual effort vs. a slightly less-well-structured graph, in many cases.

    Also, it seems that most (?) C++ developers I talk to aren’t aware of how compilers handle includes these days. I didn’t know either (except for some nonstandard pragma directives), so I’ve got some homework to do,

    Finally, I’m glad you can avoid char*. A lot of C++ and Java code I see uses too many primitive types when real objects would be better. It seems that this often forces you to follow suit and support both string and char*, in this particular case.

  5. Avatar
    Przemyslaw Owczarek about 18 hours later:

    Dean, idea with doing separate build using g++ is good but somebody long time ago allow the code in system i take care of to rot(“broken window…”) and now code is just not compiling with g+. I now that using g+ -Wall -pedantic can help me find at least few issues and improve code quality but it’s gonna be a long journey :) I need to reserve a week for that and finally make it happen.

  6. Avatar
    OBricker about 20 hours later:

    Dean,

    While std::exception does not provide a constructor with a string arg, the what() function is virtual so you could create your own exception classes that define the string or take one in the constructor and override what to use it.

    I believe that is what all the exceptions in do.

  7. Avatar
    teki321@gmail.com 1 day later:

    Obsolete Tool Support
    It is possible to implement it: http://www.wholetomato.com/products/featureChoice.asp

    Manual Memory Management
    Reflection and Metaprogramming
    Library Issues

    Use boost: http://www.boost.org/
    If you need a gc: http://www.hpl.hp.com/personal/Hans_Boehm/gc/

    Dependency Management
    This is not an easy one, but it’s possible to live with it.
    http://ccache.samba.org/ can help to reduce compilation times. It is possible to use precompiled headers (which may or may not work).

    C++ can be used in several ways. It is mostly used as C + objects, but the possibilities are really wide, it depends on the resources and the project how far you should go.
    On the other end, just do not use C++ if there are more effective ways to solve the problem. If you need the speed, create an extension/module for your favorite language.

  8. Avatar
    Kristian Dupont 1 day later:

    Przemyslaw, I think that the performance overhead that virtualized functions cause is “paying for something that I don’t need” which is almost officially against the spirit of the language :) I know that premature optimization is evil and all, but I think that when doing TDD, I use dependency injection so much that I end up making a lot of methods virtual. It may be silly, but I often use templates instead.. (But, I use C++ for game development so I am more allergic to these issues than most :)

  9. Avatar
    Jonathan Wakely 1 day later:

    > The file will still get opened and read in its entirety every time it is included.

    This is not true of any modern compilers. At my previous job I worked with John Lakos, who recommends “redundant include guards” in his 1996 book, but a colleague re-ran his tests and found that only one compiler in use at the firm actually re-opened the files, and that compiler had been put out to pasture by the vendor. Newer versions of that compiler, and all others in use at the firm, recognise the include guards for what they are and will cache a list of headers that have been seen and don’t need to be re-opened.

    GCC has done this for about 10 years, since at least version 2.95, here are the docs for the latest release: http://gcc.gnu.org/onlinedocs/gcc-4.2.0/cpp/Once_002dOnly-Headers.html

    EDG documents this feature in their compiler front-end. I’m pretty sure Sun’s compiler has had a similar optimisation since Forte 6, maybe earlier.

  10. Avatar
    Ben 1 day later:

    I think you’re missing a fair bit. For example:

    • Visual Assist for Visual Studio has great on-the-fly highlighting of errors, and recently, refactoring. I hate coding without it.
    • Most compilers have a precompiled header option, some also have the ‘once’ pragma.
    • Interfaces are simply abstract base classes, without member variables. That’s how they’re (generally) implemented in other languages; they are not against “the spirit of C++”.

    I also think you’re on the wrong track making a new .exe for each test. They should be grouped together, to reduce your overhead.

    Memory management is just part of C+, but something that if done well (and using smart_pointer from Boost, for example), doesn’t need to be that painful. This has little to do with TDD, although you could (and probably should) have some memory leak tests. I will say, in my years of C/C+ development, finding memory leaks (and I used to work with consoles, where no leaks were allowed) rarely cost a significant amount of time, although tools & techniques helped considerably.

    I’ll admit C++ can be annoying, and without reflection (and with header files) TDD is a lot more work. But it’s not an order of magnitude.

  11. Avatar
    OBricker 1 day later:

    OOPs. comments ate my angle-bracketed reference to stdexcept. I had tried to say:

    I believe that is what all the exceptions in “stdexcept” do.

  12. Avatar
    Jonathan Wakely 1 day later:

    > C++ emphasizes performance and backwards-compatibility with C over all other considerations.

    I think if you asked the ISO committee members you’d get a number of very different answers. I would say backwards-compatibility with C++ is at least as important!

    I agree with Ben above, except that #pragma once should be avoided, it’s non-portable, and most compilers do the same thing automatically anyway.

    OBricker’s right too, you should use the derived types in stdexcept (or your own types derived from std::exception) rather than throwing std::exception directly. Throw a derived type, catch by reference-to-base type – there’s an obvious analogy to dealing with polymorphic types through base class interfaces.

    Why would you overload on const std::string& and const char* ? You can pass a const char* to the string version directly. There might be cases where you want to avoid the temporary std::string that this creates, but they will probably be the uncommon case … I certainly think it’s easy to avoid writing those overloads.

    Complaints about tool support are certainly valid. The complex grammar is due to the C ancestry and it does mean tools that need to understand the grammar need a full-blown compiler front-end. GDB’s simple parser just can’t handle complex template instantiations. Eclipse/CDT does include some limited refactoring tools, and work is ongoing c.f. http://ifs.hsr.ch/downloads/Flyer_Cerp_eng.pdf

  13. Avatar
    Dean Wampler 1 day later:

    Ben, You and several of the other posters pointed me to some tools that I didn’t know about, for which I’m grateful.

    Just one other comment about memory management. I heard recently of some anecdotal evidence that overuse of reference-counting smart pointers can actually be a bottleneck, because of all the “counting” overhead. I’m not saying these tools are “evil” or anything like that ;), just that no solution is without consequences and we should always profile our applications!

  14. Avatar
    AnthonyMoralez 1 day later:

    I agree with almost all of your points. However, regarding Dependency Management, external include guards aren’t necessary. g++ remembers if a header has internal include guards and skips the extraneous file I/O when they encounter it a second time [Llopis] . I know not every C++ project is built with g++, but for many teams the external include guards aren’t necessary.

  15. Avatar
    David Paxson 1 day later:

    I’ve used Ref++ for refactoring support in Visual Studio 2003. It works quite well, even on C only code, for the refactorings it supports. I have mostly used rename, extract method, change signature, and introduce variable.

  16. Avatar
    Tim 1 day later:

    I’ve done some work making it easier to TDD in vim. I have a set of macros and abbreviations that really shorten the time to get code running.

    I absolutely do not have separate exes for my .cpp files. I like them grouped up. I also do not create any header files for my tests. Header files are for the benefit of someone who wants to include them, and there is no such person in CPPUnit testing. I write all tests inline in the class declaration, so there is only the registration line and the function. I then have a hot-key map to register my test functions (and to remove them utterly).

    I will probably create a macro for renaming test functions. It won’t be as nice as a rename refactoring, but it would be helpful. As is, I use ”*” to highlight the name and jump to the next usage, CWnewname to change the first, then * to jump to the next usage and dot (.) to change it also. It usually takes me all of two seconds, and I do it enough to matter.

    If you use the CompilerOutputter and build a single executable, your VIM editor becomes an IDE. Not Eclipse, maybe, nor even quite EMACS, but more than usable and even quite handy.

    I also incorporated valgrind into my builds so that memory management problems are much much easier to spot and fix.

    It’s not so hard to set up compared with configuring a Java project especially. But I would still rather be working in a more modern dynamic language like python. It’s hard to compete with zero setup, though.

    +1 on always deriving an exception class from the std::exception. I don’t like throwing the languages’ exceptions for my problems.

    I haven’t used any good C++ refactoring tools, sadly.

  17. Avatar
    Torbjörn Kalin 1 day later:

    @Ben: I agree with Dean that TDD in C++ is probably an order of magnitude slower than in Java.

    I agree with you that Visual Assist is quite good at refactorings, although it is far, far from as powerful as Eclipse. Also, Eclipse is extremely powerful when it comes to generating code, something that saves a lot of time.

    However, what’s really powerful IMO when TDDing in Java is jMock. In C++ you have to manually create every mock class. With jMock, that’s done with a single line of code. That really saves time, and removes a lot of duplication.

    With this said, I still love (and hate) C++ as a language :)

  18. Avatar
    Ricardo Mayerhofer 5 months later:

    I work in a Company were most of the code is writen in C++, we use agile and have no trouble using TDD. I’m wondering what we do different.

  19. Avatar
    Przemyslaw Owczarek 9 months later:

    Ricardo, do you mind sharing what tools do you use on unix platforms? We do TDD as well but it’s kinda slow as we do everything by hand (with a great help from VIM).

  20. Avatar
    Jarl Friis about 1 year later:

    In addition to the other tools mentoined amop is also a great tool when practising TDD with C++.

  21. Avatar
    DVD to HTC Converter over 3 years later:

    thank you for sharing the post ,it is really good PDF to BMP Converter is the

    excellent combination of super high conversion speed and perfect output quality. At the same time, PDF to BMP Converter supports

    various output formats like: JPEG, TIFF, TGA, RLE, EMF, WMF and so on.

  22. Avatar
    Maurice Yaws over 3 years later:

    Thanks for this great post, i like this article very informative.

  23. Avatar
    Reinaldo Osario over 3 years later:

    Awesome post! My windows at home broke last night, just had to get http://www.zh-bamboo.com them fixed – i feel your pain!

  24. Avatar
    bag manufacturer over 3 years later:

    TDD in C++ with some Java. While C++ was my language of choice through m

  25. Avatar
    Indonesian Teak Furniture: Indoor Teak Furniture, Teak Garden Furniture, Teak Table, Teak Chairs Indonesian Teak Furniture: Indoor Teak Furniture, Teak Garden Furniture, Teak Table, Teak Chairs over 3 years later:

    Indonesian Teak Furniture: Indoor Teak Furniture, Teak Garden Furniture, Teak Table, Teak Chairs

  26. Avatar
    Pandora over 3 years later:

    has just told him that hand-washing is too expensive, and he should stop doing it.”

  27. Avatar
    Delaware Mortgage Modification over 3 years later:

    Thanks for sharing such a good article, I like your point of view, this matter sorted out your thoughtful blogger exchange is my honor, I wish I could, and you become good friends, for more a more in-depth exchanges! Delaware Mortgage Modification

  28. Avatar
    viagra over 3 years later:

    You bring out some really good points in your post. viagra

  29. Avatar
    iPad to Mac over 3 years later:

    over and over again, I like to come to here, thanks.

  30. Avatar
    Silicone Molding over 3 years later:

    Intertech Machinery Inc. provides the most precise Plastic Injection Mold and Rubber Molds from Taiwan. With applying excellent unscrewing device in molds, Intertech is also very professional for making flip top Cap Molds in the world.

  31. Avatar
    hublot replicas over 4 years later:

    to a busy signal.”While breitling bentley swiss replica signal.”While I was thinking yesterday that I Mens Rings I would reschedule, now I am just rolex yacht master replica just thinking: Give

  32. Avatar
    Criminal Records over 4 years later:

    Better reflection and metaprogramming support would permit more robust proxy or aspect solutions to be used.

  33. Avatar
    dswehfhh over 4 years later:

    We are the professional jacket manufacturer, jacket supplier, jacket factory, welcome you to custom jacket.

  34. Avatar
    Sunglass over 4 years later:

    Women Replica Sunglass at cheap discount price Inspired ,MEN designer Sunglasses

  35. Avatar
    SEO Firm India over 4 years later:

    hey man,i am a first time visitor here and like your blog.

  36. Avatar
    jaychouchou over 4 years later:

    To be, or not to be- that is a question.Whether ipad bag tis nobler in the mind to suffer The slings and Game Controllers arrows of outrageous fortune Or to take arms against a sea of troubles, And USB Gadgets by opposing end them.

  37. Avatar
    Diamond Reviews over 4 years later:

    Tremendous work. I am getting benefit from your post. So i have to share thank you. You can go long way by your skill.

  38. Avatar
    damper over 4 years later:

    Our company is engaged in the professional manufacturer of damper, air cylinder, oil cylinder, and hydraulic station. The company has many year’s production experience and strong technical power.

  39. Avatar
    okey oyunu oyna over 4 years later:

    i can not say anything good article…

    Gerçek ki?ilerle sohbet ederek okey oyunu oyna ve internette online oyun oynaman?n zevkini ç?kar.

  40. Avatar
    Best Phone Lookup over 4 years later:

    I visited this page first time and found it Very Good Job of acknowledgment and a marvelous source of info…......Thanks Admin!

  41. Avatar
    Best Phone Lookup over 4 years later:

    I visited this page first time and found it Very Good Job of acknowledgment and a marvelous source of info…......Thanks Admin!

  42. Avatar
    christian louboutin shoes on sale over 4 years later:

    Have the christian louboutin patent leather pumps is a happy thing. Here have the most complete kinds of christian louboutin leather platform pumps.

  43. Avatar
    Jewellery over 4 years later:

    Online UK costume and fashion jewellery shop with, g

  44. Avatar
    beats by dr dre headphones over 4 years later:

    Beats by dr dre studio with look after talk in white. extra attributes on Monster Beats By Dr. Dre Pro Headphones Black a specific tri-fold design and design and carrying circumstance which make for compact and uncomplicated safe-keeping when not in use. Beats by dr dre solo .

  45. Avatar
    cheap soccer jersey over 4 years later:

    you really have avery nice blog,it’s the first time to be here but it won’t be the last untill then keep blogging.goodluck!

  46. Avatar
    ????? ????? over 4 years later:

    ? ?? ?? ? ? ?? ?? ? ??? ?? ?? ?? ? . ?? ?? ???? ?? ? ??? ???? ??? ??? ???? ?? ?? . ? ?? ?? ??? ??? ?? ?? ??? ??? .

  47. Avatar
    canada goose coat over 4 years later:

    Canada Goose Outlet is Marmot 8000M Parka. The Marmot 8000M Parka is really a waterproof, breathable jacket with 800 fill canada goose jacket feathers. It truly is design and light colored shell is produced for trendy, but uncomplicated, protection from cold temperatures. Reinforced shoulders, elbows and adjustable waist and hem make the Marmot a perfect alternate for skiing and other outdoor sports that want fairly a bit of arm motion. The 8000M Parka weighs three lbs., comes in bonfire and black colours and might be stuffed and stored like a sleeping bag to your convenience.This is one of well-know and prime down jacket brands.Hope our friends like its!Like canada goose womens and Canada Goose Expedition Parka.There are wholesale canada goose.

  48. Avatar
    ysbearing over 4 years later:

    Slewing bearing called slewing ring bearings, is a comprehensive load to bear a large bearing, can bear large axial, radial load and overturning moment.

  49. Avatar
    credit calculators over 4 years later:

    nice to see this site is very good information. thanks for sharing.

  50. Avatar
    Tips For Bowling over 4 years later:

    The cause of justice is the cause of humanity. Its advocates should overflow with universal good will. We should love this cause, for it conduces to the general happiness of mankind.

  51. Avatar
    christian louboutin over 4 years later:

    Christian Louboutin Rolando Hidden-Platform Pumps Golden is a fashion statement – that’s sexy, it makes you look longer highlight, and it highlights the curves in the woman body and makes the body look more elegant and thinner without any diet.

    ?Brand: Christian Louboutin ?Material: Golden leather ?Specialty: Signature red sole ?Color: Golden ?Heel height: Approximately 130mm/ 5.2 inches high and a concealed 20mm/ 1 inch platform ?Condition: Brand New in box with dust bags & Original Box

    Fashion, delicate, luxurious Christian louboutins shoes on sale, one of its series is Christian Louboutin Rolando Pumps, is urbanism collocation. This Christian louboutins shoes design makes people new and refreshing. Red soles shoes is personality, your charm will be wonderful performance.

  52. Avatar
    http://www.cheapnfljerseys001.com/ over 4 years later:

    thank you man.

  53. Avatar
    http://www.supplycheapjersey.com/ over 4 years later:

    NFL,MLB,NBA,NHL,SOCCER Sunglasses

  54. Avatar
    ????? over 4 years later:

    thank you sow mach

  55. Avatar
    iPhone sms transfer over 5 years later:

    well. you guys really give us the sample of C++ programing skill. So. why not try this method and do a better code. next time. have another try.

  56. Avatar
    ??????? ??????? over 5 years later:

    nice to see this site is very good information. thanks for sharing.

    ??? ???

Comments