diff --git a/.gitignore b/.gitignore index c35a4df1..60f60e46 100644 --- a/.gitignore +++ b/.gitignore @@ -29,8 +29,9 @@ *.app # IDE-specific -.*/ -cmake-build-*/ +.idea/ +.vscode/ +*build*/ # Conan *.pyc diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..86d0aaef --- /dev/null +++ b/.travis.yml @@ -0,0 +1,34 @@ +env: + global: + - CONAN_USERNAME: "mpusz" + - CONAN_LOGIN_USERNAME: "mpusz" + - CONAN_CHANNEL: "testing" + - CONAN_UPLOAD: "https://api.bintray.com/conan/mpusz/conan-mpusz " + - CONAN_STABLE_BRANCH_PATTERN: "release/*" + - CONAN_UPLOAD_ONLY_WHEN_STABLE: 0 + - CONAN_DOCKER_32_IMAGES: 1 + +linux: &linux + os: linux + dist: xenial + language: python + python: "3.7" + services: + - docker +osx: &osx + os: osx + language: generic +matrix: + include: + - <<: *linux + env: CONAN_GCC_VERSIONS=7 CONAN_DOCKER_IMAGE=conanio/gcc7 + - <<: *linux + env: CONAN_GCC_VERSIONS=8 CONAN_DOCKER_IMAGE=conanio/gcc8 + +install: + - chmod +x .travis/install.sh + - ./.travis/install.sh + +script: + - chmod +x .travis/run.sh + - ./.travis/run.sh diff --git a/.travis/install.sh b/.travis/install.sh new file mode 100644 index 00000000..00eaf926 --- /dev/null +++ b/.travis/install.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -e +set -x + +if [[ "$(uname -s)" == 'Darwin' ]]; then + brew update || brew update + brew outdated pyenv || brew upgrade pyenv + brew install pyenv-virtualenv + brew install cmake || true + + if which pyenv > /dev/null; then + eval "$(pyenv init -)" + fi + + pyenv install 3.7.1 + pyenv virtualenv 3.7.1 conan + pyenv rehash + pyenv activate conan +fi + +pip install conan_package_tools +pip install conan --upgrade +conan user diff --git a/.travis/run.sh b/.travis/run.sh new file mode 100644 index 00000000..0a3488ee --- /dev/null +++ b/.travis/run.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e +set -x + +if [[ "$(uname -s)" == 'Darwin' ]]; then + if which pyenv > /dev/null; then + eval "$(pyenv init -)" + fi + pyenv activate conan +fi + +python build.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d9fe9db..c243aab9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,6 @@ include(compile_flags) add_subdirectory(src) # add unit tests -enable_testing() add_subdirectory(test) # add usage example diff --git a/LICENSE b/LICENSE.md similarity index 97% rename from LICENSE rename to LICENSE.md index 068aaab3..ccafbdb2 100644 --- a/LICENSE +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018 Mateusz Pusz +Copyright (c) 2016 Mateusz Pusz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4a641188..9c8d8fbe 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?maxAge=3600)](https://raw.githubusercontent.com/mpusz/units/master/LICENSE) +[![Travis CI](https://img.shields.io/travis/mpusz/units/master.svg?label=Travis%20CI)](https://travis-ci.org/mpusz/units) +[![AppVeyor](https://img.shields.io/appveyor/ci/mpusz/units/master.svg?label=AppVeyor)](https://ci.appveyor.com/project/mpusz/units) +[![Download](https://api.bintray.com/packages/mpusz/conan-mpusz/units%3Ampusz/images/download.svg)](https://bintray.com/mpusz/conan-mpusz/units%3Ampusz/_latestVersion) + # `units` - Physical Units Library for C++ ## Summary -`Units` is a compile-time enabled Modern C++ library that provides support for converting physical -units and dimensions. The basic idea and design heavily bases on `std::chrono::duration` and extends -it to work properly with many dimensions. +`Units` is a compile-time enabled Modern C++ library that provides compile-time dimensional +analysis and unit/quantity manipulation. The basic idea and design heavily bases on +`std::chrono::duration` and extends it to work properly with many dimensions. Here is a small example of possible operations: @@ -19,444 +24,25 @@ static_assert(2_kmph * 2_h == 4_km); static_assert(2_km / 2_kmph == 1_h); ``` -## Requirements -1. Safety and performance - - strong types - - template metaprogramming - - `constexpr` all the things -2. The best possible user experience - - compiler errors - - debugging -3. No macros in the user interface -4. Easy extensibility -5. No external dependencies -6. Possibility to be standardized as a part of the C++ Standard Library +## Repository structure +That repository contains the following `cmake`-based projects: + - `./src` - header-only project for `units` + - `.` - project used for development needs that wraps `./src` project together with + usage examples and unit tests + - `./test_package` - library installation and conan package verification + +Please note that all projects depend on some `cmake` modules in `./cmake` directory. -## Basic Concepts -### `Dimensions` +## Building, testing and installation -`units::dimension` is a type-list like type that stores an ordered list of exponents of one -or more base dimensions: +For detailed information on project compilation, testing and reuse please refer to +[doc/INSTALL.md](doc/INSTALL.md). -```cpp -template -struct dimension : upcast_base> {}; -``` -`units::Dimension` is a Concept that is satisfied by a type that is empty and publicly -derived from `units::dimension` class template: +## Library design -```cpp -template -concept Dimension = - std::is_empty_v && - detail::is_dimension && // exposition only - DerivedFrom; -``` - -#### `Exponents` - -`units::exp` provides an information about a single base dimension and its exponent in a derived -dimension: - -```cpp -template -struct exp { - using dimension = BaseDimension; - static constexpr int value = Value; -}; -``` - -where `BaseDimension` is a unique sortable compile-time value and for now is implemented as: - -```cpp -template -using dim_id = std::integral_constant; -``` - -but it is meant to be replaced with C++20 class `constexpr` values provided as non-type template -parameters (when feature will be available in a compiler) so that for example base dimension for -length will be expressed as `dimension>`. - -`units::Exponent` concept is satisfied if provided type is an instantiation of `units::exp` class -template: - -```cpp -template -concept Exponent = - detail::is_exp; // exposition only -``` - -#### `make_dimension` - -Above design of dimensions is created with the ease of use for end users in mind. Compile-time -errors should provide as short as possible template instantiations strings that should be easy to -understand by every engineer. Also types visible in a debugger should be easy to understand. -That is why `units::dimension` type for derived dimensions always stores information about only -those base dimensions that are used to form that derived dimension. - -However, such an approach have some challenges: - -```cpp -constexpr Velocity auto v1 = 1_m / 1_s; -constexpr Velocity auto v2 = 2 / 2_s * 1_m; - -static_assert(Same); -static_assert(v1 == v2); -``` - -Above code, no matter what is the order of the base dimensions in an expression forming our result, -must produce the same `Velocity` type so that both values can be easily compared. In order to achieve -that, `dimension` class templates should never be instantiated manually but through a `make_dimension_t` -template metaprogramming factory function: - -```cpp -template -struct make_dimension { - using type = /* unspecified */; -}; - -template -using make_dimension_t = typename make_dimension::type; -``` - -So for example to create a `dimension_velocity` type we have to do: - -```cpp -struct dimension_velocity : make_dimension_t, exp> {}; -``` - -In order to make `make_dimension_t` work as expected it has to provide unique ordering for -contained base dimensions. Beside providing ordering to base dimensions it also has to: -- aggregate two arguments of the same base dimension but different exponents -- eliminate two arguments of the same base dimension and with opposite equal exponents - -Additionally, it would be good if the final type produced by `make_dimension_t` would be easy to -understand for the user. For example we may decide to order base dimensions with decreasing order of -their exponents. That is why second sorting of a type list may be required. For example: - -```cpp -template -struct make_dimension { - using type = mp::type_list_sort_t, exp_dim_id_less>>, exp_greater_equal>; -}; -``` - - -#### `merge_dimension` - -`units::merge_dimension` is similar to `make_dimension` but instead of sorting the whole list -of base dimensions from scratch it assumes that provided input `dimension` types are already -sorted as a result of `make_dimension`. - -Typical use case for `merge_dimension` is to produce final `dimension` return type of multiplying -two different dimensions: - -```cpp -template -struct dimension_multiply; - -template -struct dimension_multiply, dimension> { - using type = upcasting_traits_t, dimension>>; -}; - -template -using dimension_multiply_t = typename dimension_multiply::type; -``` - -Example implementation of `merge_dimension` may look like: - -```cpp -template -struct merge_dimension { - using type = mp::type_list_sort_t>, exp_greater_equal>; -}; -``` - - -### `Units` - -`units::unit` is a class template that expresses the unit of a specific physical dimension: - -```cpp -template - requires (R::num > 0) -struct unit : upcast_base> { - using dimension = D; - using ratio = R; -}; -``` - -`units::Unit` is a Concept that is satisfied by a type that is empty and publicly -derived from `units::unit` class template: - -```cpp -template -concept Unit = - std::is_empty_v && - detail::is_unit && // exposition only - DerivedFrom; -``` - -### `Quantities` - -`units::quantity` is a class template that expresses the quantity/amount of a specific dimension -expressed in a specific unit of that dimension: - -```cpp -template - requires Same -class quantity; -``` - -`units::Quantity` is a Concept that is satisfied by a type that is an instantiation of `units::quantity` -class template: - -```cpp -template -concept Quantity = - detail::is_quantity; // exposition only -``` - -`units::quantity` provides the interface really similar to `std::chrono::duration` with additional -member types and functions as below: - -```cpp -template - requires Same -class quantity { -public: - using dimension = D; - using unit = U; - - template - requires treat_as_floating_point> || std::ratio_multiply::den == 1 - quantity, upcasting_traits_t, std::ratio_multiply>>, std::common_type_t> - constexpr operator*(const quantity& lhs, - const quantity& rhs); - - template - quantity, upcasting_traits_t, std::ratio>>, std::common_type_t> - constexpr operator/(const Rep1& v, - const quantity& q); - - template - requires treat_as_floating_point> || std::ratio_divide::den == 1 - quantity, upcasting_traits_t, std::ratio_divide>>, std::common_type_t> - constexpr operator/(const quantity& lhs, - const quantity& rhs); -}; -``` - -Additional functions provide the support for operations that result in a different dimension type -than those of their arguments. - -#### `quantity_cast` - -To explicitly force truncating conversions `quantity_cast` function is provided which is a direct -counterpart of `std::chrono::duration_cast`. - -## Strong types instead of aliases, and type upcasting capability - -Most of the important design decisions in the library are dictated by the requirement of providing -the best user experience as possible. - -For example with template aliases usage the following code: - -```cpp -const Velocity t = 20_s; -``` - -could generate a following compile time error: - -```text -C:\repos\units\example\example.cpp:39:22: error: deduced initializer does not satisfy placeholder constraints - const Velocity t = 20_s; - ^~~~ -In file included from C:\repos\units\example\example.cpp:23: -C:/repos/units/src/include/units/si/velocity.h:41:16: note: within 'template concept const bool units::Velocity [with T = units::quantity >, units::unit >, std::ratio<1> >, long long int>]' - concept Velocity = Quantity && Same; - ^~~~~~~~ -In file included from C:/repos/units/src/include/units/bits/tools.h:25, - from C:/repos/units/src/include/units/dimension.h:25, - from C:/repos/units/src/include/units/si/base_dimensions.h:25, - from C:/repos/units/src/include/units/si/velocity.h:25, - from C:\repos\units\example\example.cpp:23: -C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: within 'template concept const bool mp::std_concepts::Same [with T = units::dimension >; U = units::dimension, units::exp >]' - concept Same = std::is_same_v; - ^~~~ -C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: 'std::is_same_v' evaluated to false -``` - -Time and velocity are not that complicated dimensions and there are much more complicated dimensions -out there, but even for those dimensions - -```text -[with T = units::quantity >, units::unit >, std::ratio<1> >, long long int>] -``` - -and - -```text -[with T = units::dimension >; U = units::dimension, units::exp >] -``` - -starts to be really hard to analyze or debug. - -That is why it was decided to provide automated upcasting capability when possible. With that the -same code will result with such an error: - -```text -C:\repos\units\example\example.cpp:40:22: error: deduced initializer does not satisfy placeholder constraints - const Velocity t = 20_s; - ^~~~ -In file included from C:\repos\units\example\example.cpp:23: -C:/repos/units/src/include/units/si/velocity.h:48:16: note: within 'template concept const bool units::Velocity [with T = units::quantity]' - concept Velocity = Quantity && Same; - ^~~~~~~~ -In file included from C:/repos/units/src/include/units/bits/tools.h:25, - from C:/repos/units/src/include/units/dimension.h:25, - from C:/repos/units/src/include/units/si/base_dimensions.h:25, - from C:/repos/units/src/include/units/si/velocity.h:25, - from C:\repos\units\example\example.cpp:23: -C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: within 'template concept const bool mp::std_concepts::Same [with T = units::dimension_time; U = units::dimension_velocity]' - concept Same = std::is_same_v; - ^~~~ -C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: 'std::is_same_v' evaluated to false -``` - -Now - -```text -[with T = units::quantity] -``` - -and - -```text -[with T = units::dimension_time; U = units::dimension_velocity] -``` - -are not arguably much easier to understand thus provide better user experience. - -Upcasting capability is provided through dedicated `upcasting_traits` and by `base_type` member -type in `upcast_base` class template. - -```cpp -template -struct upcasting_traits : std::type_identity {}; - -template -using upcasting_traits_t = typename upcasting_traits::type; -``` - -```cpp -struct dimension_length : make_dimension_t> {}; - -template<> -struct upcasting_traits : - std::type_identity {}; -``` - -```cpp -struct kilometer : unit {}; - -template<> -struct upcasting_traits : - std::type_identity {}; -``` - - -## Adding new dimensions - -In order to extend the library with custom dimensions the user has to: -1. Create a new dimension type and provide upcasting trait for it: - -```cpp -struct dimension_velocity : make_dimension_t, exp> {}; -template<> struct upcasting_traits : std::type_identity {}; -``` - -2. Define the base unit (`std::ratio<1>`) and secondary ones and provide upcasting traits for them via: - -```cpp -struct meter_per_second : unit> {}; -template<> struct upcasting_traits : std::type_identity {}; -``` - -3. Define a concept that will match a new dimension: - -```cpp -template -concept Velocity = Quantity && Same; -``` - -4. Provide user-defined literals for the most important units: - -```cpp -namespace literals { - constexpr auto operator""_mps(unsigned long long l) { return velocity(l); } - constexpr auto operator""_mps(long double l) { return velocity(l); } -} -``` - - -## Adding new base dimensions - -For now base dimensions are defined in terms of `std::integral_constant` and the provided -values must be unique. For example: - -```cpp -struct base_dim_length : dim_id<0> {}; -struct base_dim_mass : dim_id<1> {}; -struct base_dim_time : dim_id<2> {}; -struct base_dim_electric_current : dim_id<3> {}; -struct base_dim_temperature : dim_id<4> {}; -struct base_dim_amount_of_substance : dim_id<5> {}; -struct base_dim_luminous_intensity : dim_id<6> {}; -``` - -However, as soon as C++20 class type values will be supported as non-type template parameters -base dimensions will be just a text values. For example: - -```cpp -inline constexpr base_dim base_dim_length = "length"; -``` - -With that it should be really easy to add support for any new non-standard base units to the -library without the risk of collision with any dimension type defined by the library itself or -by other users extending the library with their own dimension types. - -Additionally, it should make the error logs even shorter thus easier to understand. - - -## Open questions - -1. Should we ensure that dimension is always a result of make_dimension? How to do it? - -2. Should we provide strong types and upcasting_traits for `quantity` type? - - In such a case all the operators have to be provided to a child class. Or maybe use CRTP? - -3. What to do with `time` which is ambiguous? - -4. What to do with `std::chrono::duration`? - -5. What is the best way to add support for temperatures? - - Temperatures require not only require `std::ratio` but also should adjusted/shifted by some - constant values (i.e. [°C] = [K] − 273.15). - -6. Should the "base dimension" be better expressed/isolated by the design? - -7. `seconds` or `time`? - -8. How to use CTAD? - - CTAD for alias templates were already supported by EWG in San Diego 2018 so `length(3.5)` - will work. However,deduction with partial argument lists was rejected so `length(3)` - will not be supported for now. +`units` library design rationale and documentation can be found in +[doc/DESIGN.md](doc/DESIGN.md) diff --git a/build.py b/build.py new file mode 100644 index 00000000..a289b6a3 --- /dev/null +++ b/build.py @@ -0,0 +1,6 @@ +from cpt.packager import ConanMultiPackager + +if __name__ == "__main__": + builder = ConanMultiPackager() + builder.add_common_builds(shared_option_name=False, pure_c=False) + builder.run() diff --git a/conanfile.py b/conanfile.py index d91d4f78..8d820ee4 100644 --- a/conanfile.py +++ b/conanfile.py @@ -26,21 +26,38 @@ class UnitsConan(ConanFile): name = "units" version = "0.0.1" author = "Mateusz Pusz" - license = "https://github.com/mpusz/units/blob/master/LICENSE" + license = "https://github.com/mpusz/units/blob/master/LICENSE.md" url = "https://github.com/mpusz/units" description = "Physical Units library for C++" + exports = ["LICENSE.md"] settings = "os", "compiler", "build_type", "arch" requires = ( "cmcstl2/2019.03.18@mpusz/stable", "gsl-lite/0.33.0@nonstd-lite/stable" ) + scm = { + "type": "git", + "url": "auto", + "revision": "auto" + } generators = "cmake" - def build(self): + def _configure_cmake(self): cmake = CMake(self) cmake.configure(source_dir="%s/src" % self.source_folder) + return cmake + + def build(self): + cmake = self._configure_cmake() cmake.build() - # cmake.install() + + def package(self): + self.copy(pattern="*license*", dst="licenses", excludes="cmake/common/*", ignore_case=True, keep_path=False) + cmake = self._configure_cmake() + cmake.install() def package_info(self): - self.cpp_info.libs = ["units"] + self.cpp_info.includedirs = ['include'] + + def package_id(self): + self.info.header_only() diff --git a/doc/DESIGN.md b/doc/DESIGN.md new file mode 100644 index 00000000..5c800b3e --- /dev/null +++ b/doc/DESIGN.md @@ -0,0 +1,498 @@ +# `units` - Physical Units Library for C++ + +## Summary + +`Units` is a compile-time enabled Modern C++ library that provides compile-time dimensional +analysis and unit/quantity manipulation. The basic idea and design heavily bases on +`std::chrono::duration` and extends it to work properly with many dimensions. + +Here is a small example of possible operations: + +```cpp +static_assert(1000 / 1_s == 1_kHz); +static_assert(1_h == 3600_s); +static_assert(1_km + 1_m == 1001_m); +static_assert(10_km / 5_km == 2); +static_assert(10_km / 2 == 5_km); +static_assert(1_km / 1_s == 1000_mps); +static_assert(2_kmph * 2_h == 4_km); +static_assert(2_km / 2_kmph == 1_h); +``` + + +## Requirements + +1. Safety and performance + - strong types + - template metaprogramming + - `constexpr` all the things +2. The best possible user experience + - compiler errors + - debugging +3. No macros in the user interface +4. Easy extensibility +5. No external dependencies +6. Possibility to be standardized as a part of the C++ Standard Library + + +## Basic Concepts + +### `Dimensions` + +`units::dimension` is a type-list like type that stores an ordered list of exponents of one +or more base dimensions: + +```cpp +template +struct dimension : upcast_base> {}; +``` + +`units::Dimension` is a Concept that is satisfied by a type that is empty and publicly +derived from `units::dimension` class template: + +```cpp +template +concept Dimension = + std::is_empty_v && + detail::is_dimension && // exposition only + DerivedFrom; +``` + +#### `Exponents` + +`units::exp` provides an information about a single base dimension and its exponent in a derived +dimension: + +```cpp +template +struct exp { + using dimension = BaseDimension; + static constexpr int value = Value; +}; +``` + +where `BaseDimension` is a unique sortable compile-time value and for now is implemented as: + +```cpp +template +using dim_id = std::integral_constant; +``` + +but it is meant to be replaced with C++20 class `constexpr` values provided as non-type template +parameters (when feature will be available in a compiler) so that for example base dimension for +length will be expressed as `dimension>`. + +`units::Exponent` concept is satisfied if provided type is an instantiation of `units::exp` class +template: + +```cpp +template +concept Exponent = + detail::is_exp; // exposition only +``` + +#### `make_dimension` + +Above design of dimensions is created with the ease of use for end users in mind. Compile-time +errors should provide as short as possible template instantiations strings that should be easy to +understand by every engineer. Also types visible in a debugger should be easy to understand. +That is why `units::dimension` type for derived dimensions always stores information about only +those base dimensions that are used to form that derived dimension. + +However, such an approach have some challenges: + +```cpp +constexpr Velocity auto v1 = 1_m / 1_s; +constexpr Velocity auto v2 = 2 / 2_s * 1_m; + +static_assert(Same); +static_assert(v1 == v2); +``` + +Above code, no matter what is the order of the base dimensions in an expression forming our result, +must produce the same `Velocity` type so that both values can be easily compared. In order to achieve +that, `dimension` class templates should never be instantiated manually but through a `make_dimension_t` +template metaprogramming factory function: + +```cpp +template +struct make_dimension { + using type = /* unspecified */; +}; + +template +using make_dimension_t = typename make_dimension::type; +``` + +So for example to create a `dimension_velocity` type we have to do: + +```cpp +struct dimension_velocity : make_dimension_t, exp> {}; +``` + +In order to make `make_dimension_t` work as expected it has to provide unique ordering for +contained base dimensions. Beside providing ordering to base dimensions it also has to: +- aggregate two arguments of the same base dimension but different exponents +- eliminate two arguments of the same base dimension and with opposite equal exponents + +Additionally, it would be good if the final type produced by `make_dimension_t` would be easy to +understand for the user. For example we may decide to order base dimensions with decreasing order of +their exponents. That is why second sorting of a type list may be required. For example: + +```cpp +template +struct make_dimension { + using type = mp::type_list_sort_t, exp_dim_id_less>>, exp_greater_equal>; +}; +``` + + +#### `merge_dimension` + +`units::merge_dimension` is similar to `make_dimension` but instead of sorting the whole list +of base dimensions from scratch it assumes that provided input `dimension` types are already +sorted as a result of `make_dimension`. + +Typical use case for `merge_dimension` is to produce final `dimension` return type of multiplying +two different dimensions: + +```cpp +template +struct dimension_multiply; + +template +struct dimension_multiply, dimension> { + using type = upcasting_traits_t, dimension>>; +}; + +template +using dimension_multiply_t = typename dimension_multiply::type; +``` + +Example implementation of `merge_dimension` may look like: + +```cpp +template +struct merge_dimension { + using type = mp::type_list_sort_t>, exp_greater_equal>; +}; +``` + + +### `Units` + +`units::unit` is a class template that expresses the unit of a specific physical dimension: + +```cpp +template + requires (R::num > 0) +struct unit : upcast_base> { + using dimension = D; + using ratio = R; +}; +``` + +`units::Unit` is a Concept that is satisfied by a type that is empty and publicly +derived from `units::unit` class template: + +```cpp +template +concept Unit = + std::is_empty_v && + detail::is_unit && // exposition only + DerivedFrom; +``` + +### `Quantities` + +`units::quantity` is a class template that expresses the quantity/amount of a specific dimension +expressed in a specific unit of that dimension: + +```cpp +template + requires Same +class quantity; +``` + +`units::Quantity` is a Concept that is satisfied by a type that is an instantiation of `units::quantity` +class template: + +```cpp +template +concept Quantity = + detail::is_quantity; // exposition only +``` + +`units::quantity` provides the interface really similar to `std::chrono::duration` with additional +member types and functions as below: + +```cpp +template + requires Same +class quantity { +public: + using dimension = D; + using unit = U; + + template + requires treat_as_floating_point> || std::ratio_multiply::den == 1 + quantity, upcasting_traits_t, std::ratio_multiply>>, std::common_type_t> + constexpr operator*(const quantity& lhs, + const quantity& rhs); + + template + quantity, upcasting_traits_t, std::ratio>>, std::common_type_t> + constexpr operator/(const Rep1& v, + const quantity& q); + + template + requires treat_as_floating_point> || std::ratio_divide::den == 1 + quantity, upcasting_traits_t, std::ratio_divide>>, std::common_type_t> + constexpr operator/(const quantity& lhs, + const quantity& rhs); +}; +``` + +Additional functions provide the support for operations that result in a different dimension type +than those of their arguments. + +#### `quantity_cast` + +To explicitly force truncating conversions `quantity_cast` function is provided which is a direct +counterpart of `std::chrono::duration_cast`. + +## Strong types instead of aliases, and type upcasting capability + +Most of the important design decisions in the library are dictated by the requirement of providing +the best user experience as possible. + +For example with template aliases usage the following code: + +```cpp +const Velocity t = 20_s; +``` + +could generate a following compile time error: + +```text +C:\repos\units\example\example.cpp:39:22: error: deduced initializer does not satisfy placeholder constraints + const Velocity t = 20_s; + ^~~~ +In file included from C:\repos\units\example\example.cpp:23: +C:/repos/units/src/include/units/si/velocity.h:41:16: note: within 'template concept const bool units::Velocity [with T = units::quantity >, units::unit >, std::ratio<1> >, long long int>]' + concept Velocity = Quantity && Same; + ^~~~~~~~ +In file included from C:/repos/units/src/include/units/bits/tools.h:25, + from C:/repos/units/src/include/units/dimension.h:25, + from C:/repos/units/src/include/units/si/base_dimensions.h:25, + from C:/repos/units/src/include/units/si/velocity.h:25, + from C:\repos\units\example\example.cpp:23: +C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: within 'template concept const bool mp::std_concepts::Same [with T = units::dimension >; U = units::dimension, units::exp >]' + concept Same = std::is_same_v; + ^~~~ +C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: 'std::is_same_v' evaluated to false +``` + +Time and velocity are not that complicated dimensions and there are much more complicated dimensions +out there, but even for those dimensions + +```text +[with T = units::quantity >, units::unit >, std::ratio<1> >, long long int>] +``` + +and + +```text +[with T = units::dimension >; U = units::dimension, units::exp >] +``` + +starts to be really hard to analyze or debug. + +That is why it was decided to provide automated upcasting capability when possible. With that the +same code will result with such an error: + +```text +C:\repos\units\example\example.cpp:40:22: error: deduced initializer does not satisfy placeholder constraints + const Velocity t = 20_s; + ^~~~ +In file included from C:\repos\units\example\example.cpp:23: +C:/repos/units/src/include/units/si/velocity.h:48:16: note: within 'template concept const bool units::Velocity [with T = units::quantity]' + concept Velocity = Quantity && Same; + ^~~~~~~~ +In file included from C:/repos/units/src/include/units/bits/tools.h:25, + from C:/repos/units/src/include/units/dimension.h:25, + from C:/repos/units/src/include/units/si/base_dimensions.h:25, + from C:/repos/units/src/include/units/si/velocity.h:25, + from C:\repos\units\example\example.cpp:23: +C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: within 'template concept const bool mp::std_concepts::Same [with T = units::dimension_time; U = units::dimension_velocity]' + concept Same = std::is_same_v; + ^~~~ +C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: 'std::is_same_v' evaluated to false +``` + +Now + +```text +[with T = units::quantity] +``` + +and + +```text +[with T = units::dimension_time; U = units::dimension_velocity] +``` + +are not arguably much easier to understand thus provide better user experience. + +Upcasting capability is provided through dedicated `upcasting_traits` and by `base_type` member +type in `upcast_base` class template. + +```cpp +template +struct upcasting_traits : std::type_identity {}; + +template +using upcasting_traits_t = typename upcasting_traits::type; +``` + +```cpp +struct dimension_length : make_dimension_t> {}; + +template<> +struct upcasting_traits : + std::type_identity {}; +``` + +```cpp +struct kilometer : unit {}; + +template<> +struct upcasting_traits : + std::type_identity {}; +``` + + +## Adding new dimensions + +In order to extend the library with custom dimensions the user has to: +1. Create a new dimension type and provide upcasting trait for it: + +```cpp +struct dimension_velocity : make_dimension_t, exp> {}; +template<> struct upcasting_traits : std::type_identity {}; +``` + +2. Define the base unit (`std::ratio<1>`) and secondary ones and provide upcasting traits for them via: + +```cpp +struct meter_per_second : unit> {}; +template<> struct upcasting_traits : std::type_identity {}; +``` + +3. Define a concept that will match a new dimension: + +```cpp +template +concept Velocity = Quantity && Same; +``` + +4. Provide user-defined literals for the most important units: + +```cpp +namespace literals { + constexpr auto operator""_mps(unsigned long long l) { return velocity(l); } + constexpr auto operator""_mps(long double l) { return velocity(l); } +} +``` + + +## Adding new base dimensions + +For now base dimensions are defined in terms of `std::integral_constant` and the provided +values must be unique. For example: + +```cpp +struct base_dim_length : dim_id<0> {}; +struct base_dim_mass : dim_id<1> {}; +struct base_dim_time : dim_id<2> {}; +struct base_dim_electric_current : dim_id<3> {}; +struct base_dim_temperature : dim_id<4> {}; +struct base_dim_amount_of_substance : dim_id<5> {}; +struct base_dim_luminous_intensity : dim_id<6> {}; +``` + +However, as soon as C++20 class type values will be supported as non-type template parameters +base dimensions will be just a text values. For example: + +```cpp +inline constexpr base_dim base_dim_length = "length"; +``` + +With that it should be really easy to add support for any new non-standard base units to the +library without the risk of collision with any dimension type defined by the library itself or +by other users extending the library with their own dimension types. + +Additionally, it should make the error logs even shorter thus easier to understand. + + +## Open questions + +1. Should we ensure that dimension is always a result of `make_dimension`? How to do it? + +2. Should we provide strong types and upcasting_traits for `quantity` type? + + In such a case all the operators have to be provided to a child class. Or maybe use CRTP? + +3. What to do with `time` which is ambiguous (conflict wit ANSI C)? + +4. What to do with `std::chrono::duration`? Is it possible to make it derive from + `quantity` which will most probably an ABI break? Alternatively, + should we provide specialization of `quantity` to work with/covnert + from/to `std::duration`? + +5. Should we provide `seconds` or stay with `time`? What about CTAD problem + for `units::length d3(3);`? + +6. What is the best way to add support for temperatures? + + Temperatures not only require `std::ratio` but also should be adjusted/shifted by some + constant values (i.e. [°C] = [K] − 273.15). + +7. Should we use `units::multiply` or stay with `std::ratio` for multiplication? + +8. Should we consider making `units::multiply` and `units::offset` a non-class template parameters + as they provide different ratio values rather than types? + + In example instead: + + ```cpp + struct celsius : unit>> {}; + ``` + + we could think about something like: + + ```cpp + struct celsius : unit>> {}; + ``` + +9. Do we need non-linear scale? + +10. Should we provide cmath-like functions for quantities? + +11. What should be the resulting type of `auto d = 1_km + 1_ft;`? + +12. Should we require explicit casts (i.e. quantity_cast) between different systems of + measurement? + +13. Should we provide Boost-like support for a `quantity_cast` to a reference that allows + direct access to the underlying value of a quantity variable? + +14. What should be the default representation (integral or `double`)? + +15. Provide ostream overloads to print quantity units (use `std::format`)? + +16. Should we provide support for dimensionless quantities? + + Because dimensionless quantities have no associated units, they behave as normal scalars, + and allow implicit conversion to and from the underlying value type or types that are + convertible to/from that value type. diff --git a/doc/INSTALL.md b/doc/INSTALL.md new file mode 100644 index 00000000..3b1df4e0 --- /dev/null +++ b/doc/INSTALL.md @@ -0,0 +1,96 @@ +# Installation and Reuse + +There are a few different ways of installing/reusing `units` in your project + +## Copy + +As `units` is a header-only library you can simply copy `src/include` directory to +your source tree and use it as regular header files. + +NOTE: Until C++20 arrives the library has some 3rd party dependencies that provide +experimental C++20 features. They can be easily obtained with conan + +```python +requires = ( + "cmcstl2/2019.03.18@mpusz/stable", + "gsl-lite/0.33.0@nonstd-lite/stable" +) +``` + +## cmake + +To use `units` as a `cmake` imported library via `cmake` configuration files the following +steps may be done. + +### cmake install + +```bash +$ mkdir build && cd build +$ cmake ../src -DCMAKE_INSTALL_PREFIX= +$ cmake --build . --target install +``` + +To use such `cmake` target in your project it is enough to add following line to your +`CMakeList.txt` file + +```cmake +find_package(units CONFIG REQUIRED) +``` + +and configure it with + +```bash +$ cmake .. -DCMAKE_INSTALL_PREFIX= +``` + +### cmake + conan + +To use `units` with `cmake` via `conan` it is enough to: +- add the following remotes to your local `conan` instance + +```bash +$ conan remote add conan-mpusz https://bintray.com/mpusz/conan-mpusz +$ conan remote add conan-nonstd https://api.bintray.com/conan/martinmoene/nonstd-lite +``` + +- add the following dependency to your `conanfile.txt` or `conanfile.py` files + +```python +requires = "units/0.0.1@mpusz/testing" +``` + +- install conan dependencies before configuring cmake + +```bash +$ cd build +$ conan install .. -pr -b=outdated -u +``` + + +# Full build and unit testing + +In case you would like to build all the code in that repository (with unit tests and examples) +you should use `CMakeLists.txt` from the parent directory. + +```bash +mkdir build && cd build +conan install .. +cmake .. +cmake --build . +``` + + +# Packaging + +To create a `conan` package and test `cmake` installation and `conan` packaging run: + +```bash +$ conan create . / --build=outdated +``` + + +# Upload package to conan server + +```bash +$ conan upload -r --all units/0.0.1@/ +``` diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index e9e89cd8..b458a68f 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -20,23 +20,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -cmake_minimum_required(VERSION 3.8) -project(units_example) - -# set path to custom cmake modules -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../cmake/common/cmake") - -# include common tools and workarounds -include(tools) - -# add dependencies -if(NOT TARGET mp::units) - find_package(units CONFIG REQUIRED) -endif() - # example app add_executable(example example.cpp) target_link_libraries(example PRIVATE - mp::units + units::units ) diff --git a/example/example.cpp b/example/example.cpp index d5ec64d7..2729bf33 100644 --- a/example/example.cpp +++ b/example/example.cpp @@ -23,26 +23,40 @@ #include "units/si/velocity.h" #include -using namespace units; +namespace { -template -void foo(V v, T t) +using namespace units::literals; + +template +constexpr units::Velocity avg_speed(D d, T t) { - const Length distance = v * t; - std::cout << "A car driving " << v.count() << " km/h in a time of " << t.count() << " minutes will pass " - << quantity_cast>(distance).count() << " meters.\n"; + return d / t; } -void foo() +template +void example_1(V v, T t) { - using namespace units::literals; - foo(60_kmph, 10.0_min); + const units::Length distance = v * t; + std::cout << "A car driving " << v.count() << " km/h in a time of " << t.count() << " minutes will pass " + << units::quantity_cast>(distance).count() << " meters.\n"; +} + +void example_2(double distance_v, double duration_v) +{ + units::length distance(distance_v); + units::time duration(duration_v); + const auto kmph = avg_speed(distance, duration); + std::cout << "Average speed of a car that makes " << distance.count() << " km in " + << duration.count() << " hours is " << kmph.count() << " km/h.\n"; +} + } int main() { try { - foo(); + example_1(60_kmph, 10.0_min); + example_2(220, 2); } catch (const std::exception& ex) { std::cerr << "Unhandled std exception caught: " << ex.what() << '\n'; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dcdcde01..2cfe56c8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,9 +21,11 @@ # SOFTWARE. cmake_minimum_required(VERSION 3.8) +#cmake_policy(SET CMP0076 NEW) + project(units VERSION 0.0.1 - LANGUAGES C CXX + LANGUAGES CXX ) # set path to custom cmake modules @@ -32,20 +34,21 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/common/cmake # include common tools and workarounds include(tools) -# add dependencies -# find_package(...) - # library definition add_library(units INTERFACE) #target_sources(units INTERFACE -# include/units/units.h -# include/units/length.h -# include/units/time.h +# include/units/dimension.h +# include/units/quantity.h +# include/units/unit.h # -# include/units/bits/common_type.h -# include/units/bits/dimensions.h -# include/units/bits/quantity.h # include/units/bits/tools.h +# include/units/bits/type_list.h +# +# include/units/si/base_dimensions.h +# include/units/si/frequency.h +# include/units/si/length.h +# include/units/si/time.h +# include/units/si/velocity.h #) target_compile_features(units INTERFACE cxx_std_20) target_link_libraries(units @@ -59,7 +62,7 @@ target_include_directories(units $ $ ) -add_library(mp::units ALIAS units) +add_library(units::units ALIAS units) # installation info install(TARGETS units EXPORT ${CMAKE_PROJECT_NAME}Targets @@ -74,4 +77,4 @@ install(DIRECTORY include/units ) # generate configuration files and install the package -configure_and_install(../cmake/common/cmake/simple_package-config.cmake.in SameMajorVersion) +configure_and_install(../cmake/common/cmake/simple_package-config.cmake.in units SameMajorVersion) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 47a22405..be55ec63 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,21 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -cmake_minimum_required(VERSION 3.8) -project(units_test) - -# set path to custom cmake modules -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../cmake/common/cmake") - -# include common tools and workarounds -include(tools) - -# add dependencies -enable_testing() -if(NOT TARGET mp::units) - find_package(units CONFIG REQUIRED) -endif() - # unit tests add_library(unit_tests test_dimension.cpp @@ -45,9 +30,5 @@ add_library(unit_tests ) target_link_libraries(unit_tests PRIVATE - mp::units -) -add_test(NAME units.unit_tests - COMMAND - unit_tests + units::units ) diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt new file mode 100644 index 00000000..4711c0f2 --- /dev/null +++ b/test_package/CMakeLists.txt @@ -0,0 +1,40 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Mateusz Pusz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +cmake_minimum_required(VERSION 2.8.12) +project(test_package) + +set(CMAKE_VERBOSE_MAKEFILE TRUE) + +include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) +conan_basic_setup(TARGETS) + +find_package(units CONFIG REQUIRED) + +# test conan-generated target +add_executable(${PROJECT_NAME}_conan test_package.cpp) +target_compile_features(${PROJECT_NAME}_conan PRIVATE cxx_std_20) # conan is not able to propagate that yet :-( +target_link_libraries(${PROJECT_NAME}_conan PRIVATE CONAN_PKG::units) + +# test cmake target +add_executable(${PROJECT_NAME}_cmake test_package.cpp) +target_link_libraries(${PROJECT_NAME}_cmake PRIVATE units::units) diff --git a/test_package/conanfile.py b/test_package/conanfile.py new file mode 100644 index 00000000..7e21df3f --- /dev/null +++ b/test_package/conanfile.py @@ -0,0 +1,52 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Mateusz Pusz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from conans import ConanFile, CMake, tools, RunEnvironment +import os + +class TestPackageConan(ConanFile): + settings = "os", "compiler", "build_type", "arch" + generators = "cmake" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def imports(self): + self.copy("*.dll", dst="bin", src="bin") + self.copy("*.dylib*", dst="bin", src="lib") + self.copy('*.so*', dst='bin', src='lib') + + def _test_run(self, bin_path): + if self.settings.os == "Windows": + self.run(bin_path) + elif self.settings.os == "Macos": + self.run("DYLD_LIBRARY_PATH=%s %s" % (os.environ.get('DYLD_LIBRARY_PATH', ''), bin_path)) + else: + self.run("LD_LIBRARY_PATH=%s %s" % (os.environ.get('LD_LIBRARY_PATH', ''), bin_path)) + + def test(self): + if not tools.cross_building(self.settings): + with tools.environment_append(RunEnvironment(self).vars): + self._test_run(os.path.join("bin", "test_package_conan")) + self._test_run(os.path.join("bin", "test_package_cmake")) diff --git a/test_package/test_package.cpp b/test_package/test_package.cpp new file mode 100644 index 00000000..f8a2c4c7 --- /dev/null +++ b/test_package/test_package.cpp @@ -0,0 +1,37 @@ +// The MIT License (MIT) +// +// Copyright (c) 2018 Mateusz Pusz +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include +#include + +using namespace units::literals; + +template +constexpr units::Velocity avg_speed(D d, T t) +{ + return d / t; +} + +int main() +{ + std::cout << "Average speed = " << avg_speed(240.0_km, 2_h).count() << " kmph\n"; +}