diff --git a/00-Table_of_Contents.md b/00-Table_of_Contents.md index 67008eb..7f2b1a3 100644 --- a/00-Table_of_Contents.md +++ b/00-Table_of_Contents.md @@ -11,5 +11,3 @@ 10. [Enable Scripting](10-Enable_Scripting.md) 11. [Further Reading](11-Further_Reading.md) 12. [Final Thoughts](12-Final_Thoughts.md) - - diff --git a/02-Use_the_Tools_Available.md b/02-Use_the_Tools_Available.md index 15c4df7..83e8cb8 100644 --- a/02-Use_the_Tools_Available.md +++ b/02-Use_the_Tools_Available.md @@ -27,14 +27,15 @@ Use an industry standard widely accepted build tool. This prevents you from rein * [Waf](https://waf.io/) * [FASTBuild](http://www.fastbuild.org/) * [Ninja](https://ninja-build.org/) - Can greatly improve the incremental build time of your larger projects. Can be used as a target for CMake. - * [Bazel](http://bazel.io/) - Fast incremental builds using network artefact caching and remote execution. - * [Buck](http://buckbuild.com/) - Similar to Bazel, with very good support for iOS and Andoid. + * [Bazel](http://bazel.io/) - Fast incremental builds using network artifact caching and remote execution. + * [Buck](http://buckbuild.com/) - Similar to Bazel, with very good support for iOS and Android. * [gyp](https://chromium.googlesource.com/external/gyp/) - Google's build tool for chromium. * [maiken](https://github.com/Dekken/maiken) - Crossplatform build tool with Maven-esque configuration style. * [Qt Build Suite](http://doc.qt.io/qbs/) - Crossplatform build tool From Qt. - * [meson](http://mesonbuild.com/index.html) - Open source build system meant to be both extremely fast, and, even more importantly, as user friendly as possible. - * [premake](https://premake.github.io/) + * [meson](http://mesonbuild.com/index.html) - Open source build system meant to be both extremely fast, and, even more importantly, as user-friendly as possible. + * [premake](https://premake.github.io/) * [xmake](https://xmake.io) - A cross-platform build utility based on Lua. Modern C/C++ build tools, Support multi-language hybrid compilation + * [build2](https://build2.org) - A cargo-like complete toolchain (build system, package manager, project manager) Remember, it's not just a build tool, it's also a programming language. Try to maintain good clean build scripts and follow the recommended practices for the tool you are using. @@ -46,9 +47,10 @@ Package management is an important topic in C++, with currently no clear winner. * [hunter](https://github.com/ruslo/hunter) - CMake driven cross-platform package manager for C/C++ * [C++ Archive Network (CPPAN)](https://cppan.org/) - a crossplatform dependency manager for C++ * [qpm](https://www.qpm.io/) - Package manager for Qt - * [build2](https://build2.org/) - cargo-like package management for C++ + * [build2](https://build2.org/) - A cargo-like complete toolchain (build system, package manager, project manager) * [Buckaroo](https://buckaroo.pm) - Truly decentralized cross-platform dependency manager for C/C++ and more - * [Vcpkg](https://github.com/microsoft/vcpkg) - Microsoft C++ Library Manager for Windows, Linux, and MacOS - [description](https://docs.microsoft.com/en-us/cpp/build/vcpkg) + * [Vcpkg](https://github.com/microsoft/vcpkg) - Microsoft C++ Library Manager for Windows, Linux, and macOS - [description](https://docs.microsoft.com/en-us/cpp/build/vcpkg) + * [CPM](https://github.com/cpm-cmake/CPM.cmake) - CMake package manager for modern CMake ## Continuous Integration @@ -86,7 +88,7 @@ Continuous Integration (CI) tools automatically build the source code as changes If you have an open source, publicly-hosted project on GitHub: - * go enable Travis Ci and AppVeyor integration right now. We'll wait for you to come back. For a simple example of how to enable it for your C++ CMake-based application, see here: https://github.com/ChaiScript/ChaiScript/blob/master/.travis.yml + * go enable github actions. A template for this can be found in the [C++ Boilerplate Template](https://github.com/cpp-best-practices/cmake_conan_boilerplate_template) * enable one of the coverage tools listed below (Codecov or Coveralls) * enable [Coverity Scan](https://scan.coverity.com) @@ -101,8 +103,9 @@ You should use as many compilers as you can for your platform(s). Each compiler ### GCC / Clang -`-Wall -Wextra -Wshadow -Wnon-virtual-dtor -pedantic` +`-Wall -Wextra -Wshadow -Wnon-virtual-dtor -pedantic` - use these and consider the following (see descriptions below) + * `-pedantic` - Warn on language extensions * `-Wall -Wextra` reasonable and standard * `-Wshadow` warn the user if a variable declaration shadows one from a parent context * `-Wnon-virtual-dtor` warn the user if a class with virtual functions has a non-virtual destructor. This helps catch hard to track down memory errors @@ -119,9 +122,10 @@ You should use as many compilers as you can for your platform(s). Each compiler * `-Wlogical-op` (only in GCC) warn about logical operations being used where bitwise were probably wanted * `-Wnull-dereference` (only in GCC >= 6.0) warn if a null dereference is detected * `-Wuseless-cast` (only in GCC >= 4.8) warn if you perform a cast to the same type - * `-Wdouble-promotion` (GCC >= 4.6, Clang >= 3.8) warn if `float` is implicit promoted to `double` - * `-Wformat=2` warn on security issues around functions that format output (ie `printf`) + * `-Wdouble-promotion` (GCC >= 4.6, Clang >= 3.8) warn if `float` is implicitly promoted to `double` + * `-Wformat=2` warn on security issues around functions that format output (i.e., `printf`) * `-Wlifetime` (only special branch of Clang currently) shows object lifetime issues + * `-Wimplicit-fallthrough` Warns when case statements fall-through. (Included with `-Wextra` in GCC, not in clang) Consider using `-Weverything` and disabling the few warnings you need to on Clang @@ -149,7 +153,7 @@ Consider using `-Weverything` and disabling the few warnings you need to on Clan * `/w14549` 'operator': operator before comma has no effect; did you intend 'operator'? * `/w14555` expression has no effect; expected expression with side-effect * `/w14619` pragma warning: there is no warning number 'number' - * `/w14640` Enable warning on thread un-safe static member initialization + * `/w14640` Enable warning on thread unsafe static member initialization * `/w14826` Conversion from 'type1' to 'type_2' is sign-extended. This may cause unexpected runtime behavior. * `/w14905` wide string literal cast to 'LPSTR' * `/w14906` string literal cast to 'LPWSTR' @@ -157,7 +161,8 @@ Consider using `-Weverything` and disabling the few warnings you need to on Clan Not recommended - * `/Wall` - Also warns on files included from the standard library, so it's not very useful and creates too many extra warnings. + * `/Wall` - Not recommended for normal builds because the MSVC standard library is not `/Wall` "clean", but can be enabled to discover new warnings to enable. + * Since VS2022, `/external:anglebrackets /external:W0` can be used to turn off warnings from all headers included with angle brackets, e.g. `#include `. @@ -180,7 +185,6 @@ If you are not using a build system like that, you can consider [Build EAR](http CMake now also comes with built-in support for calling `clang-tidy` during [normal compilation](https://cmake.org/cmake/help/latest/prop_tgt/LANG_CLANG_TIDY.html). * [include-what-you-use](https://github.com/include-what-you-use), [example results](https://github.com/ChaiScript/ChaiScript/commit/c0bf6ee99dac14a19530179874f6c95255fde173) - * [clang-modernize](http://clang.llvm.org/extra/clang-modernize.html), [example results](https://github.com/ChaiScript/ChaiScript/commit/6eab8ddfe154a4ebbe956a5165b390ee700fae1b) * [clang-check](http://clang.llvm.org/docs/ClangCheck.html) * [clang-tidy](http://clang.llvm.org/extra/clang-tidy.html) @@ -188,6 +192,12 @@ CMake now also comes with built-in support for calling `clang-tidy` during [norm The best bet is the static analyzer that you can run as part of your automated build system. Cppcheck and clang meet that requirement for free options. +### SonarLint / SonarQube / SonarCloud + +[SonarLint](https://www.sonarsource.com/products/sonarlint/) runs as a plug-in in all the main IDEs. The other two run as part of your build pipeline and can also gate PRs. [SonarQube](https://www.sonarsource.com/products/sonarqube/) runs on your own infrastructure whereas [SonarCloud](https://www.sonarsource.com/products/sonarcloud/) runs in The Cloud, is free for public Open Source projects and supports Automatic Analysis for zero-config setup. + +All three run the same set of analyzers (although SonarQube and SonarCloud have a handful of additional checks that are too heavyweight to run in-IDE) that catch code smells and best practice violations, as well as complex bugs. + ### Coverity Scan [Coverity](https://scan.coverity.com/) has a free (for open source) static analysis toolkit that can work on every commit in integration with [Travis CI](http://travis-ci.org) and [AppVeyor](http://www.appveyor.com/). @@ -203,7 +213,7 @@ Notes: * For correct work it requires well formed path for headers, so before usage don't forget to pass: `--check-config`. * Finding unused headers does not work with `-j` more than 1. - * Remember to add `--force` for code with a lot number of `#ifdef` if you need check all of them. + * Remember to add `--force` for code with a lot number of `#ifdef` if you need to check all of them. ### cppclean @@ -253,7 +263,11 @@ Qt Creator can plug into the clang static analyzer. ### IKOS [IKOS](https://ti.arc.nasa.gov/opensource/ikos/) is an open source static analyzer, developed by NASA. It is based on the Abstract Interpretation. It is written in C++ and provides an analyzer for C and C++, using LLVM. -The source code is [available on Github](https://github.com/NASA-SW-VnV/ikos). +The source code is [available on GitHub](https://github.com/NASA-SW-VnV/ikos). + +### codespell + +[codespell](https://github.com/codespell-project/codespell) is a spell checker for your source code. ## Runtime Checkers @@ -281,7 +295,7 @@ A coverage analysis tool shall be run when tests are executed to make sure the e * [Valgrind](http://www.valgrind.org/) * Valgrind is a runtime code analyzer that can detect memory leaks, race conditions, and other associated problems. It is supported on various Unix platforms. * [Heaptrack](https://github.com/KDE/heaptrack) - * A profiler created by a Valgrind's Massif developper. Quite similar to Massif with pros and cons over it, way more intuitive though. + * A profiler created by a Valgrind's Massif developer. Quite similar to Massif with pros and cons over it, way more intuitive though. * [Dr Memory](http://www.drmemory.org) * [Memoro](https://epfl-vlsc.github.io/memoro/) - A detailed heap profiler. @@ -293,6 +307,7 @@ A coverage analysis tool shall be run when tests are executed to make sure the e ### Reverse engineering tools * [Cutter](https://cutter.re/) - A front-end for [Radare2](https://www.radare.org/n/radare2.html). It provides tools such as decompiler, disassembly, graph visualizer, hex editor. + * [Ghidra](https://ghidra-sre.org/) - Ghidra is a free and open source reverse engineering tool developed by the National Security Agency (NSA) of the United States. ### GCC / Clang Sanitizers @@ -329,6 +344,7 @@ These tools take code executed during unit test runs and mutate the executed cod * [MuCPP](https://neptuno.uca.es/redmine/projects/mucpp-mutation-tool/wiki) * [mull](https://github.com/mull-project/mull) * [CCMutator](https://github.com/markus-kusano/CCMutator) + * [Universal Mutator](https://github.com/agroce/universalmutator) ### Control Flow Guard @@ -338,10 +354,6 @@ MSVC's [Control Flow Guard](https://msdn.microsoft.com/en-us/library/windows/des * `_GLIBCXX_DEBUG` with GCC's implementation libstdc++ implementation. See [Krister's blog article](https://kristerw.blogspot.se/2018/03/detecting-incorrect-c-stl-usage.html). -### Heap Profiling - - * [Memoro](https://epfl-vlsc.github.io/memoro/) - A detailed heap profiler - ## Ignoring Warnings If it is determined by team consensus that the compiler or analyzer is warning on something that is either incorrect or unavoidable, the team will disable the specific error to as localized part of the code as possible. @@ -350,7 +362,7 @@ Be sure to reenable the warning after disabling it for a section of code. You do ## Testing -CMake, mentioned above, has a built in framework for executing tests. Make sure whatever build system you use has a way to execute tests built in. +CMake, mentioned above, has a built-in framework for executing tests. Make sure whatever build system you use has a way to execute tests built in. To further aid in executing tests, consider a library such as [Google Test](https://github.com/google/googletest), [Catch](https://github.com/philsquared/Catch), [CppUTest](https://github.com/cpputest/cpputest) or [Boost.Test](http://www.boost.org/doc/libs/release/libs/test/) to help you organize the tests. @@ -376,6 +388,12 @@ Don't forget to make sure that your error handling is being tested and works pro [rr](http://rr-project.org/) is a free (open source) reverse debugger that supports C++. + +## Documentation Tools + + * [hdoc](https://hdoc.io/) the modern documentation tool for C++ + + ## Other Tools ### Lizard @@ -410,6 +428,6 @@ Don't forget to make sure that your error handling is being tested and works pro [pahole](https://linux.die.net/man/1/pahole) generates data on holes in the packing of data structures and classes in compiled code. It can also the size of structures and how they fit within the system's cache lines. -### BinSkim +### BinSkim -[BinSkim](https://github.com/Microsoft/binskim) is a binary static analysis tool that provides security and correctness results for Windows Portable Executable and *nix ELF binary formats +[BinSkim](https://github.com/Microsoft/binskim) is a binary static analysis tool that provides security and correctness results for Windows Portable Executable and *nix ELF binary formats diff --git a/03-Style.md b/03-Style.md index 241f5c7..7f8607d 100644 --- a/03-Style.md +++ b/03-Style.md @@ -35,7 +35,7 @@ Every IDE and many editors have support for clang-format built in or easily inst C++ Standard Library (and other well-known C++ libraries like [Boost](http://www.boost.org/)) use these guidelines: * Macro names use upper case with underscores: `INT_MAX`. - * Template parameter names use camel case: `InputIterator`. + * Template parameter names use Pascal case: `InputIterator`. * All other names use snake case: `unordered_map`. ## Distinguish Private Object Data @@ -240,7 +240,7 @@ It also makes it possible to have two separate files next to each other on one s ``` ## Initialize Member Variables -...with the member initializer list. +...with the member initializer list. For POD types, the performance of an initializer list is the same as manual initialization, but for other types there is a clear performance gain, see below. @@ -288,8 +288,8 @@ private: }; // Good Idea -// The default constructor for m_myOtherClass is never called here, so -// there is a performance gain if MyOtherClass is not is_trivially_default_constructible. +// The default constructor for m_myOtherClass is never called here, so +// there is a performance gain if MyOtherClass is not is_trivially_default_constructible. class MyClass { public: @@ -454,4 +454,3 @@ The Rule of Zero states that you do not provide any of the functions that the co The goal is to let the compiler provide optimal versions that are automatically maintained when more member variables are added. [This article](http://www.nirfriedman.com/2015/06/27/cpp-rule-of-zero/) provides a background and explains techniques for implementing nearly 100% of the time. - diff --git a/04-Considering_Safety.md b/04-Considering_Safety.md index 7136767..26a87bb 100644 --- a/04-Considering_Safety.md +++ b/04-Considering_Safety.md @@ -2,7 +2,7 @@ ## Const as Much as Possible -`const` tells the compiler that a variable or method is immutable. This helps the compiler optimize the code and helps the developer know if a function has a side effect. Also, using `const &` prevents the compiler from copying data unnecessarily. The [comments on `const` from John Carmack](http://kotaku.com/454293019) are also a good read. +`const` tells the compiler that a variable or method is immutable. This helps the compiler optimize the code and helps the developer know if a function has a side effect. Also, using `const &` prevents the compiler from copying data unnecessarily. The [comments on `const` from John Carmack](https://web.archive.org/web/20131211065348/https://kotaku.com/454293019) are also a good read. ```cpp // Bad Idea @@ -34,9 +34,9 @@ public: * Always return by value. -references: https://github.com/lefticus/cppbestpractices/issues/21 https://twitter.com/lefticus/status/635943577328095232 +references: https://github.com/lefticus/cppbestpractices/issues/21 https://twitter.com/lefticus/status/635943577328095232 -### Do not pass and return simple types by const ref +### Do not pass and return simple types by const ref ```cpp // Very Bad Idea @@ -47,7 +47,7 @@ public: : m_int_value(t_int_value) { } - + const int& get_int_value() const { return m_int_value; @@ -69,7 +69,7 @@ public: : m_int_value(t_int_value) { } - + int get_int_value() const { return m_int_value; @@ -80,7 +80,7 @@ private: } ``` -Why? Because passing and returning by reference leads to pointer operations instead by much more faster passing values in processor registers. +Why? Because passing and returning by reference leads to pointer operations, instead of much faster passing of values in processor registers. ## Avoid Raw Memory Access @@ -101,7 +101,7 @@ auto mybuffer = std::make_unique(length); // C++14 auto mybuffer = std::unique_ptr(new char[length]); // C++11 // or for reference counted objects -auto myobj = std::make_shared(); +auto myobj = std::make_shared(); // ... // myobj is automatically freed for you whenever it is no longer used. @@ -111,7 +111,7 @@ auto myobj = std::make_shared(); Both of these guarantee contiguous memory layout of objects and can (and should) completely replace your usage of C-style arrays for many of the reasons listed for not using bare pointers. -Also, [avoid](http://stackoverflow.com/questions/3266443/can-you-use-a-shared-ptr-for-raii-of-c-style-arrays) using `std::shared_ptr` to hold an array. +Also, [avoid](http://stackoverflow.com/questions/3266443/can-you-use-a-shared-ptr-for-raii-of-c-style-arrays) using `std::shared_ptr` to hold an array. ## Use Exceptions @@ -142,4 +142,4 @@ If you have the possibility to use a compiler that supports C++11, you can use v ## Additional Resources -[How to Prevent The Next Heartbleed](http://www.dwheeler.com/essays/heartbleed.html) by David Wheeler is a good analysis of the current state of code safety and how to ensure safe code. +[How to Prevent The Next Heartbleed](https://dwheeler.com/essays/heartbleed.html) by David Wheeler is a good analysis of the current state of code safety and how to ensure safe code. diff --git a/05-Considering_Maintainability.md b/05-Considering_Maintainability.md index 4547559..2c9a250 100644 --- a/05-Considering_Maintainability.md +++ b/05-Considering_Maintainability.md @@ -33,7 +33,7 @@ Know and understand the existing C++ standard algorithms and put them to use. * See [cppreference](https://en.cppreference.com/w/cpp/algorithm) * Watch [C++ Seasoning](https://www.youtube.com/watch?v=qH6sSOr-yk8) - + Consider a call to `[]` as a potential code smell, indicating that an algorithm was not used where it could have been. diff --git a/08-Considering_Performance.md b/08-Considering_Performance.md index 784ca33..467be02 100644 --- a/08-Considering_Performance.md +++ b/08-Considering_Performance.md @@ -43,7 +43,7 @@ For more examples see [this article](http://blog2.emptycrate.com/content/templat ### Avoid Recursive Template Instantiations -Recursive template instantiations can result in a significant load on the compiler and more difficult to understand code. +Recursive template instantiations can result in a significant load on the compiler and more difficult to understand code. [Consider using variadic expansions and folds when possible instead.](http://articles.emptycrate.com/2016/05/14/folds_in_cpp11_ish.html) @@ -103,8 +103,13 @@ If on Linux, consider using the gold linker for GCC. There's no real way to know where your bottlenecks are without analyzing the code. - * http://developer.amd.com/tools-and-sdks/opencl-zone/codexl/ - * http://www.codersnotes.com/sleepy +A list of code profilers: + * [Intel VTune](https://software.intel.com/content/www/us/en/develop/tools/oneapi/components/vtune-profiler.html) + * [Coz - Causal Profiling](https://github.com/plasma-umass/coz) + * [Sleepy](http://www.codersnotes.com/sleepy) + * [Dyninst](https://dyninst.org/) + * [AMD CodeXL](https://github.com/GPUOpen-Archive/CodeXL) + * [lukestackwalker](http://lukestackwalker.sourceforge.net/) ### Simplify the Code @@ -289,11 +294,11 @@ if (MyObject obj(index); obj.good()) { ### Prefer `double` to `float`, But Test First -Depending on the situation and the compiler's ability to optimize, one may be faster over the other. Choosing `float` will result in lower precision and may be slower due to conversions. On vectorizable operations `float` may be faster if you are able to sacrifice precision. +Depending on the situation and the compiler's ability to optimize, one may be faster over the other. Choosing `float` will result in lower precision and may be slower due to conversions. On vectorizable operations `float` may be faster if you are able to sacrifice precision. `double` is the recommended default choice as it is the default type for floating point values in C++. -See this [stackoverflow](http://stackoverflow.com/questions/4584637/double-or-float-which-is-faster) discussion for some more information. +See this [stackoverflow](http://stackoverflow.com/questions/4584637/double-or-float-which-is-faster) discussion for some more information. ### Prefer `++i` to `i++` ... when it is semantically correct. Pre-increment is [faster](http://blog2.emptycrate.com/content/why-i-faster-i-c) than post-increment because it does not require a copy of the object to be made. @@ -313,7 +318,7 @@ for (int i = 0; i < 15; ++i) ``` Even if many modern compilers will optimize these two loops to the same assembly code, it is still good practice to prefer `++i`. There is absolutely no reason not to and you can never be certain that your code will not pass a compiler that does not optimize this. -You should be also aware that the compiler will not be able optimize this only for integer types and not necessarily for all iterator or other user defined types. +You should be also aware that the compiler will not be able optimize this only for integer types and not necessarily for all iterator or other user defined types. The bottom line is that it is always easier and recommended to use the pre-increment operator if it is semantically identical to the post-increment operator. ### Char is a char, string is a string @@ -353,4 +358,3 @@ Properly use the already highly optimized components of the vendor provided stan #### `in_place_t` And Related Be aware of how to use `in_place_t` and related tags for efficient creation of objects such as `std::tuple`, `std::any` and `std::variant`. - diff --git a/09-Considering_Correctness.md b/09-Considering_Correctness.md index 5bc8b61..f39af90 100644 --- a/09-Considering_Correctness.md +++ b/09-Considering_Correctness.md @@ -25,6 +25,4 @@ Consider using a typesafe library like Note that stronger typing can also allow for more compiler optimizations. -* [Sorting in C vs C++](Sorting in C vs C++.pdf) - - +* [Sorting in C vs C++](Sorting%20in%20C%20vs%20C++.pdf) diff --git a/11-Further_Reading.md b/11-Further_Reading.md index 515e16f..cc7b721 100644 --- a/11-Further_Reading.md +++ b/11-Further_Reading.md @@ -4,7 +4,7 @@ ## C++ - * https://github.com/isocpp/CppCoreGuidelines The C++ Core Guidelines are a set of tried-and-true guidelines, rules, and best practices about coding in C++ + * https://github.com/isocpp/CppCoreGuidelines The C++ Core Guidelines are a set of tried-and-true guidelines, rules, and best practices about coding in C++ * https://www.gitbook.com/book/alexastva/the-ultimate-question-of-programming-refactoring-/details - The Ultimate Question of Programming, Refactoring, and Everything * http://llvm.org/docs/CodingStandards.html - LLVM Coding Standards - very well written * http://geosoft.no/development/cppstyle.html diff --git a/12-Final_Thoughts.md b/12-Final_Thoughts.md index e7f711b..bb4b8f6 100644 --- a/12-Final_Thoughts.md +++ b/12-Final_Thoughts.md @@ -1,4 +1,3 @@ # Final Thoughts Expand your horizons and use other programming languages. Other languages have different constructs and expressions. Learning what else is out there will encourage you to be more creative with your C++ and write cleaner, more expressive code. - diff --git a/LICENSE b/LICENSE index 0bde2a8..85c55a6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,2 +1,2 @@ -This work is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. +This work is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/. diff --git a/README.md b/README.md index 5913527..ce16a60 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,6 @@ For more information please see the [Preface](01-Preface.md). This online resource is part of Jason Turner's collection of C++ Best Practices resources. * [C++ Best Practices Book](https://leanpub.com/cppbestpractices) -* [C++ Weekly YouTube Channel](https://www.youtube.com/c/JasonTurner-lefticus) +* [C++ Weekly YouTube Channel](https://www.youtube.com/user/lefticus1) * [The Ultimate CMake/C++ Starter Project](https://github.com/lefticus/cpp_starter_project/) * [Learning C++ Best Practices - O'Reilly Video](http://shop.oreilly.com/product/0636920049814.do) diff --git a/SUMMARY.md b/SUMMARY.md index 6ecc4de..c4a9540 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -12,4 +12,3 @@ * [Enable Scripting](10-Enable_Scripting.md) * [Further Reading](11-Further_Reading.md) * [Final Thoughts](12-Final_Thoughts.md) -