Changes between Version 28 and Version 29 of BestPracticeHandbook


Ignore:
Timestamp:
May 21, 2015, 11:50:55 AM (7 years ago)
Author:
Niall Douglas
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • BestPracticeHandbook

    v28 v29  
    11211121}}}
    11221122
    1123 ASIO currently has over 1000 lines of macro logic in its config.hpp with at least twelve different possible combinations, so that is 2 ^ 12 = 4096 different configurations of code paths (note some combinations may not be allowed in the source code, I didn't check). Are all of these tested equally? I actually don't know, but it seems a huge task requiring many days of testing if they are. However there is a far worse problem here: what happens if library A configures ASIO one way and library B configures ASIO a different way, ''and then a user combines both libraries A and B into the same process?''
     1123ASIO currently has over 1000 lines of macro logic in its config.hpp with at least twelve different possible combinations, so that is 2 !^ 12 = 4096 different configurations of code paths (note some combinations may not be allowed in the source code, I didn't check). Are all of these tested equally? I actually don't know, but it seems a huge task requiring many days of testing if they are. However there is a far worse problem here: '''what happens if library A configures ASIO one way and library B configures ASIO a different way, ''and then a user combines both libraries A and B into the same process?'''''
    11241124
    11251125The answer is that such a combination violates ODR, and therefore is undefined behaviour i.e. it will crash. This makes the ability to so finely configure ASIO much less useful than it could be.
    11261126
    1127 Let me therefore propose something better: allow library users to ''dependency inject'' from the outside the configuration of whether to use a STL11 dependency or its Boost equivalent. If one makes sure to encapsulate the dependency injection into a unique inline namespace, that prevents violation of ODR and therefore collision of the incompatibly configured library dependencies. If the dependent library takes care to coexist with alternative configurations and versions of itself inside the same process, this offers maximum convenience and utility to your library's users.
    1128 
    1129 
    1130 Sometimes multiple code paths are unavoidable, specifically when you make use of Boost enhanced APIs not yet in the STL. However a large amount of the time one can treat the STL11 and the Boost implementation as being ''perfectly substitutable'' for one another, so now all you need is a method for your library's users to:
    1131 
    1132 1. ''Dependency inject'' whether your library is to use a Boost implementation or the STL11 implementation for all those STL library facilities which also have equivalents in Boost. For reference, the substitutable implementations are the following STL headers:
    1133 
    1134  `array, atomic, chrono, condition_variable, functional, future, mutex, random, ratio, regex, system_error, thread, tuple, type_traits, typeindex`
    1135 
    1136 2. Enable your library when configured with (for example) Boost.Thread to coexist with your library when configured with STL11 thread in the same executable.
    1137 
    1138 3. Ideally enable your library when configured with Boost.Thread to coexist with your library when configured with STL11 thread in the same translation unit (i.e. header only library A can include your header only library configured one way, and header only library B can include your library configured the other way).
    1139 
    1140 TODO
     1127Let me therefore propose something better: allow library users to ''dependency inject'' from the outside the configuration of whether to use a STL11 dependency or its Boost equivalent. If one makes sure to encapsulate the dependency injection into a unique inline namespace, that prevents violation of ODR and therefore collision of the incompatibly configured library dependencies. If the dependent library takes care to coexist with alternative configurations and versions of itself inside the same process, this:
     1128
     1129* Forces you to formalise your dependencies (this has a major beneficial effect on design, trust me that your code enormously improves when you are forced to think correctly about this).
     1130* Offers maximum convenience and utility to your library's users.
     1131* Lets you better test your code against multiple (future) STL implementations.
     1132* Looser coupling.
     1133* Much easier upgrades later on (i.e. less maintenance).
     1134
     1135What it won't do:
     1136
     1137* Prevent API and version fragmentation.
     1138* Deal with balkanisation (i.e. two configurations of your library are islands, and cannot interoperate).
     1139
     1140In short whether the pros outweigh the cons comes down to your library's use cases, you as a maintainer, and so on. Indeed you might make use of this technique internally for your own needs, but not expose the facility to choose to your library users.
     1141
     1142So how does one implement STL dependency injection in C++ 11/14? One entirely valid approach is the ASIO one of a large config.hpp file full of macro logic which switches between Boost and the STL11 for the following header files which were added in C++ 11:
     1143
     1144||= Boost header    =|| `array.hpp` || `atomic.hpp`            || `chrono.hpp`    || `thread.hpp`         || `bind.hpp`   || `thread.hpp` || `thread.hpp` || `random.hpp`    || `ratio.hpp` || `regex.hpp` || `system/system_error.hpp` || `thread.hpp` || `tuple/tuple.hpp` || `type_traits.hpp` || no equivalent ||
     1145||= Boost namespace =|| `boost`     || `boost, boost::atomics` || `boost::chrono` || `boost`              || `boost`      || `boost`      || `boost`      || `boost::random` || `boost`     || `boost`     || `boost::system`           || `boost`      || `boost`           || `boost`           ||               ||
     1146||= STL11 header    =|| `array`     || `atomic`                || `chrono`        || `condition_variable` || `functional` || `future`     || `mutex`      || `random`        || `ratio`     || `regex`     || `system_error`            || `thread`     || `tuple`           || `type_traits`     || `typeindex`   ||
     1147||= STL11 namespace =|| `std`       || `std`                   || `std::chrono`   || `std`                || `std`        || `std`        || `std`        || `std`           || `std`       || `std`       || `std`                     || `std`        || `std`             || `std`             || `std`         ||
     1148
     1149At the time of writing, a very large proportion of STL11 APIs are perfectly substitutable with Boost i.e. they have identical template arguments, parameters and type signatures, so all you need to do is to alias either namespace `std` or namespace `boost::?` into your own library namespace as follows:
     1150
     1151{{{#!c++
     1152// In config.hpp
     1153namespace mylib
     1154{
     1155  inline namespace MACRO_UNIQUE_ABI_ID {
     1156#ifdef MYLIB_USING_BOOST_RATIO  // The external library user sets this
     1157    namespace ratio = ::boost;
     1158#else
     1159    namespace ratio = ::std;
     1160#endif
     1161  }
     1162}
     1163
     1164// To use inside namespace mylib::MACRO_UNIQUE_ABI_ID, do:
     1165ratio::ratio<2, 1> ...
     1166}}}
     1167
     1168As much as the above looks straightforward, you will find it quickly multiplies into a lot of work just as with ASIO's config.hpp. You will also probably need to do a lot of code refactoring such that every use of ratio is prefixed with a ratio namespace alias, every use of regex is prefixed with a regex namespace alias and so on. So is there an easier way?
     1169
     1170Luckily there is, and it is called [https://github.com/ned14/Boost.APIBind APIBind]. APIBind takes away a lot of the grunt work in the above, specifically:
     1171
     1172* APIBind provides ''bind files'' for the above C++ 11 header files which let you bind just the relevant ''part of'' namespace boost or namespace std into your namespace mylib. In other words, in your namespace mylib you simply go ahead and use ratio<N, D> with no namespace prefix because ratio<N, D> has been bound directly into your mylib namespace for you. [https://github.com/ned14/Boost.APIBind/tree/master/bind APIBind's bind files] essentially work as follows:
     1173
     1174 {{{#!c++
     1175// In header <ratio> the API being bound
     1176namespace std { template <intmax_t N, intmax_t D = 1> class ratio; }
     1177
     1178// Ask APIBind to bind ratio into namespace mylib
     1179#define BOOST_STL11_RATIO_MAP_NAMESPACE_BEGIN namespace mylib {
     1180#define BOOST_STL11_RATIO_MAP_NAMESPACE_END }
     1181#include BOOST_APIBIND_INCLUDE_STL11(bindlib, std, ratio)  // If you replace std with boost, you bind boost::ratio<N, D> instead.
     1182
     1183// Effect on namespace mylib
     1184namespace mylib
     1185{
     1186  template<intmax_t _0, intmax_t _1 = 1> using ratio = ::std::ratio<_0, _1>;
     1187}
     1188
     1189// You can now use mylib::ratio<N, D> without prefixing. This is usually a very easy find and replace in files operation.
     1190 }}}
     1191
     1192* APIBind provides generation of inline namespaces with an ABI and version specific mangling to ensure different dependency injection configurations do not collide:
     1193
     1194 {{{#!c++
     1195// BOOST_AFIO_V1_STL11_IMPL, BOOST_AFIO_V1_FILESYSTEM_IMPL and BOOST_AFIO_V1_ASIO_IMPL all are set to either boost or std in your config.hpp
     1196
     1197// Note the last bracketed item is marked inline. On compilers without inline namespace support this bracketed item is ignored.
     1198#define BOOST_AFIO_V1 (boost), (afio), (BOOST_BINDLIB_NAMESPACE_VERSION(v1, BOOST_AFIO_V1_STL11_IMPL, BOOST_AFIO_V1_FILESYSTEM_IMPL, BOOST_AFIO_V1_ASIO_IMPL), inline)
     1199#define BOOST_AFIO_V1_NAMESPACE       BOOST_BINDLIB_NAMESPACE      (BOOST_AFIO_V1)
     1200#define BOOST_AFIO_V1_NAMESPACE_BEGIN BOOST_BINDLIB_NAMESPACE_BEGIN(BOOST_AFIO_V1)
     1201#define BOOST_AFIO_V1_NAMESPACE_END   BOOST_BINDLIB_NAMESPACE_END  (BOOST_AFIO_V1)
     1202
     1203// From now on, instead of manually writing namespace boost { namespace afio { and boost::afio, instead do:
     1204BOOST_AFIO_V1_NAMESPACE_BEGIN
     1205  struct foo;
     1206BOOST_AFIO_V1_NAMESPACE_END
     1207
     1208// Reference struct foo from the global namespace
     1209BOOST_AFIO_V1_NAMESPACE::foo;
     1210
     1211// Alias hard version dependency into mylib
     1212namespace mylib
     1213{
     1214  namespace afio = BOOST_AFIO_V1_NAMESPACE;
     1215}
     1216 }}}
     1217
     1218* APIBind also provides boilerplate for allowing inline reconfiguration of a library during the same translation unit such that the following "just works":
     1219
     1220 {{{#!c++
     1221// test_all_multiabi.cpp in the AFIO unit tests
     1222
     1223// A copy of AFIO + unit tests completely standalone apart from Boost.Filesystem
     1224#define BOOST_AFIO_USE_BOOST_THREAD 0
     1225#define BOOST_AFIO_USE_BOOST_FILESYSTEM 1
     1226#define ASIO_STANDALONE 1
     1227#include "test_all.cpp"
     1228#undef BOOST_AFIO_USE_BOOST_THREAD
     1229#undef BOOST_AFIO_USE_BOOST_FILESYSTEM
     1230#undef ASIO_STANDALONE
     1231
     1232// A copy of AFIO + unit tests using Boost.Thread, Boost.Filesystem and Boost.ASIO
     1233#define BOOST_AFIO_USE_BOOST_THREAD 1
     1234#define BOOST_AFIO_USE_BOOST_FILESYSTEM 1
     1235// ASIO_STANDALONE undefined
     1236#include "test_all.cpp"
     1237#undef BOOST_AFIO_USE_BOOST_THREAD
     1238#undef BOOST_AFIO_USE_BOOST_FILESYSTEM
     1239 }}}
     1240
     1241 In other words, you can reset the configuration macros and reinclude afio.hpp to generate a new configuration of AFIO as many times as you like within the same translation unit. This allows header only library A to require a different configuration of AFIO than header only library B, and it all "just works". As APIBind is currently lacking documentation, I'd suggest you [https://docs.google.com/presentation/d/1badtN7A4lMzDl5i098SHKvlWsQY-tsVcutpq_UlRmFI/pub?start=false&loop=false&delayms=3000 review the C++ Now 2015 slides on the topic] until proper documentation turns up. The procedure is not hard, and you can examine https://github.com/BoostGSoC13/boost.afio/blob/master/include/boost/afio/config.hpp for a working example of it in action. Do watch out for the comments marking the stanzas which are automatically generated by scripting tools in APIBind, writing those by hand would be tedious.
    11411242
    11421243== 17. FUTURE PROOFING: Consider being C++ resumable function ready ==