4
头图

Author: Wang Dongwei (Lu Mu)

foreword

Thinking about it, the author has been engaged in cross-multi-terminal development for more than two years. Some time ago, because the cross-desktop project in the group needed to return to the development under Windows for a full two months, how to describe these two months, hehe, all kinds of "unscrupulous" Writing method, finally no need to consider the behavior of the following n terminals in writing a line of code, "labor" and "efficiency" have been greatly liberated.

However, after the release of Windows, the author is responsible for the adaptation of mac. At this stage, I found a lot of tricks and tricks that are not "compliant" (the adaptation quota was originally scheduled for 2 working days, and it took about a week. ), as a cpp programmer with a little idea, I came up with the idea of writing a guide to avoid pits in cross-multi-terminal development. I remembered Scott Meyers' "Effective C++" I read in the past.... Try to write "xx effective use of cpp" Cross-end development experience", I hope that reading this article can help you in how to keep the same cpp code in the same behavior on multiple platforms compilation and construction.

The complexity of cross-multi-end development is mostly caused by two reasons:

  1. Platform differences under multiple systems
  2. Behavioral Uncertainty under Multiple Compilers

The following will mainly explain from these two aspects.

At the same time, after reading a number of cpp programmer development books, I still feel that Google C++ Style Guide is the most effective and direct way to avoid pitfalls, and I still recommend it to everyone: https://google.github.io/styleguide/cppguide .html

Enter the text below -

1. The choice of C++ VERSION

The choice of C++ version can be said to be crucial for cross-terminal development. One of the more difficult points in cross-terminal development is how to support platform differences well under multiple platforms. With the upgrade of C++ versions, more and more new The feature is supported in the standard library, which means that developers can pay less attention to platform differences. Therefore, it is recommended to choose the latest stable version. As of now, C++17 is recommended.

2. It is forbidden to repeatedly include files in a single compilation.

This situation can be effectively avoided in two ways:

1. #pragma once, it should be noted that this is a non-standard but widely supported preprocessing symbol. In mainstream compilation, clang, ms, etc. have been supported.

 #pragma once #include<vector> ...

2. The way to use #define

 #ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... #endif // FOO_BAR_BAZ_H_

3. The problem of path and header file path separators

In Windows, the recognition of paths supports both forward and backward slashes, but in Linux, it can only be /. In addition, in Linux, paths are strictly case-sensitive, and in Windows, case is ignored.

suggestion:

  1. For paths, it is necessary to strictly ensure that the case matches the actual path.
  2. Use "\" for paths in code, use "/" instead.

This move will save a lot of work during your adaptation process from win to mac.

4. The header file of the C standard library contains

Some header files of the C standard library do not need to be explicitly included under Windows, but they need to be included under Linux. Therefore, in cross-end development, the header files required in this file should be included as much as possible in the .c and .cpp files, and this is also the standard requirement of the C language standard since C99.

5. Code file format

In cross-terminal development, especially the part containing Chinese, unless your code is all English comments, it is difficult to avoid the problem of Chinese garbled characters caused by cross-development under multi-platform (especially development under Windows and Unix-like platforms). .

Recommendation: Use UTF-8 BOM encoding format for all.

6. About inline functions

Definition: When a function is declared as an inline function, the compiler expands it inline instead of calling it by the usual function calling mechanism.

Referring to the definition, it has its own advantages. When the function body is relatively small, inlining the function can make the target code more efficient. In general, the use of inlining should be encouraged when the function is relatively short.

Regarding inline functions, many non-cross-end programmers may think that it is not enough. In fact, there are several issues that are worthy of attention in cross-end development:

  1. Excessive inlining will lead to bloated programs, especially for mobile terminals. On the one hand, the size of the C++ code cannot be solved very well, and on the other hand, it will also slow down the program.
  2. Improper use of inlining in exported header files can lead to unexpected results in cross-module development. Here is an example. When providing a cross-terminal SDK, the export header file is usually provided, but if the inline is inappropriate in the export header file, the compilation will cross from the current unit to another module, which may cause a series of problems.
  3. Although compilers optimize inline functions more or less, different compilers are not the same, and good inline usage habits can still help you in practice. For example, in a cpp project on the mobile side, by going to Inlining reduces a certain package size. Practice has proved that the compiler may not be a perfect fit during the selection process. For inline compiler optimizations, please refer to: https://isocpp.org/wiki/faq/inline-function

To sum up, to avoid using inline as much as possible in cross-end development, here are a few measurable criteria (experience value?):

  1. Inline is prohibited for more than 10 lines (google suggestion)
  2. Prohibit the use of inlining in non-get functions (experience value, this one will be more controversial, but in my opinion it is only necessary to use inlining when getting the value of a member variable, others are not necessary and may bring "surprise")
  3. Inline functions must have appropriate modifiers (const)
  4. If the destructor has custom content, it is forbidden to use inlining (google suggests, usually destructors do more than you think)

7. About the basic type definition

Please use base type definitions, custom base types are prohibited.

I have seen several code bases of the team, and some students and even three-party libraries are very fond of customization in the use of basic types, such as:

 typedef std::int8_t int8; typedef std::int16_t int16; typedef std::int32_t int32; typedef std::int64_t int64; typedef std::uint8_t uint8; typedef std::uint16_t uint16; typedef std::uint32_t uint32; typedef std::uint64_t uint64;

In cross-module development and code fusion, the customization of these basic types often has ambiguity, redefine, etc. You may say that such definitions should have their own #define protection, but most programmers will not do this , It is strongly not recommended to customize the basic type here. The standard library provides it is simple and general enough. Please take care of your teammates when you develop it yourself.

8. Definition of CHAR

The definition of char needs to show whether it is unsigned or signed.

It should be noted that char is not specified as signed or unsigned in the standard. Different compilers may have different results, and there may be unexpected results when implicit conversion occurs. For example, when char is cast to int, It is found that under the x86 platform, it is handled as signed, but under ARM32, it is treated as unsigned, which causes problems. ARM64 is normally signed. Of course, you can solve it by specifying CFLAG += fsigned-char, but such problems should be specified in the specification. be avoided.

9. Questions about wide characters

What you need to know: wchar_t occupies two bytes in Windows and four bytes in Linux, here are a few questions

  1. resulting in different sizes of volume occupancy.
  2. Program porting brings difficulties
  3. Implicit conversion result is not as expected

Cross-end development should avoid the widespread use of wchar, to avoid the overhead and additional problems caused by wide-narrow character conversion, and utf-8 should be generally used as the main encoding, which is also the mainstream idea. Even in special scenarios, you can use utf16 and avoid using wchar. In short, don't use it unless necessary.

10. It should be limited that the string array is encoded as uft-8 when it is saved as a byte stream

Please add u8"" before the string, especially the part containing Chinese. Students who are used to developing under VS also need to pay extra attention. The default file encoding of VS is gb2312, which may cause the string to be accidentally saved. For gbk encoding format.

At the same time, u8 can only be used in front of strings. It is meaningless to use it in front of characters. Even if it compiles and passes on ms, it will prompt in clang.

 int pos = targetID.rfind(u8'_'); // error: use of undeclared identifier 'u8' ...

11. Avoid the definition of two consecutive angle brackets

E.g

 std::vector<std::vector<int>> vec

It is no problem to write this under Windows, then it may not compile under some platforms. There are two ways:

1. You can leave a space between two consecutive angle bracket symbols, that is

 std::vector<std::vector<int> > vec;

2. You can also typedef

This problem has been solved in the C++11 standard, if you confirm that the compiler version already supports this feature (reference: https://isocpp.org/wiki/faq/cpp11-language-misc

In C++98 this is a syntax error because there is no space between the two >s. C++11 recognizes such two >s as a correct termination of two template argument lists.), this clause can be ignored, but usually two The case of >> also means nested use, and the readability is usually improved after typedef.

12. Code part processing for platform differences

Cross-end development will inevitably lead to platform differences. For the processing of this part, it is recommended to use the if def method to distinguish the short part. For the functional and more code, it is recommended to use separate file development, xxxx_win.cpp, xxxx_mac.cpp , xxxx_linux.cpp, you can refer to the code of chromium to use this method a lot.

At the same time, for the difference code part, the principle of not being defined unless necessary should be maintained. Because the cross-end code processing method should be maintained as much as possible, too many platform differences will inevitably lead to poor maintainability.

13. Avoid using keywords that are not supported by non-standard compilers

  1. C++ standard keyword reference: https://baike.baidu.com/item/C%2B%2B%E5%85%B3%E9%94%AE%E5%AD%97/5773813
  2. The keywords at the beginning of the double bottom bar are mostly C++ keywords defined by Microsoft, which should be avoided as much as possible in cross-end development, such as: __super, __wchar_t, __stdcall__stdcall, etc. For details, please refer to: https://docs.microsoft.com/zh -cn/cpp/cpp/keywords-cpp?view=msvc-170#microsoft-specific-c-keywords

14. Use of Assert

Assert was used as a widely (or even rotten) warning handling method in the PC era. On mobile terminals and Unix-like systems, debug performance is usually more violent than windows, usually blocking processing, especially mobile The terminal will cause the program to continue to run, unlike windows that pop up a box and give you an option to continue.

Therefore, you should avoid using assert directly in cross-end development. You can consider using the redefined assert, and at the same time use the redefined assert reasonably.

 #ifdef NDEBUG #define ALOG_ASSERT(_Expression) ((void)0) #else #define ALOG_ASSERT(_Expression) do { \ ... \ 这里可以额外做error级别日志输出,是否进行assert阻塞式处理。 if(HandleAssert()) \ { \ assert(_Expression); \ } \ } while (false) #endif

15. About Inheritance

Composition is often more appropriate than inheritance. When using inheritance, make it public.

This definition of google should still be very accurate, usually composition is more suitable than inheritance, even if it is to be used, it must be publice. You should try to use inheritance while keeping "is a", if you want to use private inheritance, you should replace it with an instance of the base class as a member object.

For overloaded virtual functions or virtual destructors, use the override, or (less commonly used) final keyword to explicitly mark it. Under some clang compilers, the compiler requires that the declaration must be displayed, otherwise an error will be reported, ms There is no such requirement.

16. About Static variables

Interested partners can study the C++ feature "Dynamic Initialization and Destruction with Concurrency", which defines the order of static and dynamic variable destructors. All objects in the thread life cycle are destructed before static variables, and static variables follow Constructed-destructed-first-destructed-stacked sequential release. In practice, it is found that apple's clang compiler and runtime library support this feature of c++11, but do not realize the multi-thread safety of static variable destructor.

Therefore, at the current stage, if global static variables are used, the problem of destructing multi-thread safety needs to be considered, otherwise crashes will occur on individual platforms online.

A relatively simple idea: replace global static variables with local static variables and do not release until the process is killed. There is also a disguised benefit here: the loading time is changed from load to the actual runtime of this code snippet.

 eg: old: static std::recursive_mutex& m_mutex; new: static std::recursive_mutex& mutex() { static std::recursive_mutex& mutex = *(new std::recursive_mutex()); return mutex; }

17. About templates

The emergence of templates has greatly facilitated programmers. Before entering the cross-terminal field, although they understood some of its criticisms (code expansion & performance loss caused by unreasonable use), they were always considered to be a very good feature. As the mobile terminal has more and more strict requirements on the package size, the use of templates is limited across terminals and needs to be used more reasonably, otherwise the expansion will be very severe. In the long process of de-templating, some experience values can be output for your reference.

  1. In cross-terminal development involving mobile terminals, templates should be avoided unless it brings enough benefits, such as json serialization, which is replaced by cjson throughout. From the perspective of development experience and code expansion ratio, the replacement is It is not worth it. For example, customizing the std standard container seems to save a lot of bloat, but the maintainability and readability of the code are greatly reduced, and it is also not worth replacing.
  2. Choose the smallest template compilation unit as possible, such as the original template class, change it to a template function in the class
  3. In general, templates can be removed in various ways. This is not to say that the method of changing the actual parameters of the template is not written.
  4. The speed of template inflation should be reduced as much as possible. In other words, if possible, the possibility of template specialization should be limited as much as possible. For example, our log serialization can be implemented for any struct or class after implementing the ToString() method. Log automation output, any type will generate a specific type of entity when it enters LOG_IMPL. After a slight modification, the type that needs to be serialized needs to display the interface class that inherits IOBJECT. Only one type (IOBJECT*) of types will be instantiated, which in practice reduces our package size by about a fifth.
  5. In multiple inheritance, especially if the public module base class contains templates, the benefit of removing templates is generally greater, because try to limit the appearance of templates in the base class, unless necessary, it should be replaced in any way.

Finally, let me say that templates are really convenient for users, but in the cross-terminal field, it seems that there are stricter requirements for template builders, and it is necessary to focus on how to avoid being inflated. In addition, the performance requirements are also more stringent. To be strict, there are many ways to provide template performance in C++11. && cooperates with std::forward to achieve perfect forwarding, etc. If you are interested, you can read "Effective Modern C++".

The above also applies to macros.

18. About the compiler

Cross-end development is bound to understand the compilers under various platforms. The main representatives here are clang, ms (also known as vs), gcc, etc. The main differences between compilers are not introduced here. You can go to google. The past and present of clang, as well as the difference between several compilers, and the corresponding platform.

As a rapidly developing compiler, clang not only has a rapid increase in compilation speed, but also has very clear error messages. It is strongly recommended that cross-end developers should use clang as the main default compiler for development if possible. Good Error prompts will greatly improve efficiency, and clang's code inspection will be more strict and standardized, which is also conducive to cross-platform compilation of code.

Let me add another sentence here. I read an article on Zhihu before comparing various compilers. When comparing clang and gcc, the first place is not the compilation speed and error prompts that we usually say, and the smaller ones. Compiled products (these are commonly known) are licenses. The limitation of gcc's GPL makes the rapid development of LLVM represented by BSD license. If it is not for this limitation, I believe that today's series of compilers represented by LLVM are all belongs to gcc.

Therefore, "As a technical student, don't think that a technical bull can conquer the world, and a precise market position can sometimes solve many problems." This sentence is quite good, and I will share with you.

19. About the conversion layer

If you do cross-module development, please stick to the principle that the conversion layer should not do any business code logic and special directional code logic.

The conversion layer is also a language glue layer, which is a code layer that converts languages from c++ to oc, c++ to java, and other languages.

Usually, after the wrapper adheres to the principle, the maintainability will be greatly improved, and it is enough to focus on the C++ code. For the language conversion layer, there are also many automatic translation tools in the industry, such as Djinni.

end

On the way to cross-end development, I have gradually grown from a novice to a full-fledged person. In addition to thanking the team for the opportunity, I am very grateful to many students, especially those from across departments, for their help along the way. Thank you, Bixin ~Finally return to the theme, the cross-end cpp development closed pit guide is far more than that, welcome to add it together. Thanks.

Pay attention to [Alibaba Mobile Technology] WeChat public account, 3 mobile technology practices & dry goods every week for you to think about!


阿里巴巴终端技术
336 声望1.3k 粉丝

阿里巴巴移动&终端技术官方账号。