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 |
| 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: |
| 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 | |
| 1135 | What 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 | |
| 1140 | In 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 | |
| 1142 | So 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 | |
| 1149 | At 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 |
| 1153 | namespace 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: |
| 1165 | ratio::ratio<2, 1> ... |
| 1166 | }}} |
| 1167 | |
| 1168 | As 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 | |
| 1170 | Luckily 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 |
| 1176 | namespace 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 |
| 1184 | namespace 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: |
| 1204 | BOOST_AFIO_V1_NAMESPACE_BEGIN |
| 1205 | struct foo; |
| 1206 | BOOST_AFIO_V1_NAMESPACE_END |
| 1207 | |
| 1208 | // Reference struct foo from the global namespace |
| 1209 | BOOST_AFIO_V1_NAMESPACE::foo; |
| 1210 | |
| 1211 | // Alias hard version dependency into mylib |
| 1212 | namespace 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. |