Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some math functions now prefixed with std:: #7918

Merged
merged 107 commits into from
May 8, 2024

Conversation

dimitre
Copy link
Member

@dimitre dimitre commented Apr 29, 2024

1 - This one starts changing every one of this functions to std::

sin, cos, acos, atan2, atan, abs, ceil, floor, round, pow to std

this way we assure it is working with floats or double automatically. old C style versions always use double, so optimizations opportunities can be discarded from the compiler. Basically lots of floats are being casted to double and the results casted from double to float again

2 - Removing all macros like MAX or M_PI_2, etc from the core, and making an optional preprocessor directive to enable old macros if needed.

3 - replacing ofRadToDeg and ofDegToRad to glm::degrees / glm::radians.
This one is almost the same, but some includes of ofMath.h can now be removed (they are removed in this PR)
Advantage: glm::degrees / radians can be applied in vectors at once, so they are updated in two different places

4 - lastly and optional. a conditional OF_USE_LEGACY_MATH_MACROS to use legacy macros

@dimitre dimitre changed the title Some math changes to std:: Some math functions now prefixed with std:: Apr 30, 2024
@dimitre dimitre mentioned this pull request Apr 30, 2024
7 tasks
@dimitre
Copy link
Member Author

dimitre commented Apr 30, 2024

I think it is good to go!

@dimitre dimitre requested a review from ofTheo April 30, 2024 14:32
Copy link
Contributor

@NickHardeman NickHardeman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @dimitre, nice work, this is a big PR. :)
Internally, using the std functions and floats makes sense, but I think in the examples it can be a bit confusing, ie. mixing ints and floats with std::min or std::max
Maybe a ofMin(float value1, float value2 ) and ofMax( float value1, float value2); could help remedy the issue so that it's cast to a float, but you can pass in double, int or float to it. Thoughts?

examples/gl/multiTexture3dExample/src/ofApp.cpp Outdated Show resolved Hide resolved
examples/input_output/imageSequenceExample/src/ofApp.cpp Outdated Show resolved Hide resolved
examples/sound/audioOutputExample/src/ofApp.cpp Outdated Show resolved Hide resolved
libs/openFrameworks/gl/shaders/pbrLighting.glsl Outdated Show resolved Hide resolved
@dimitre
Copy link
Member Author

dimitre commented May 7, 2024

Yes, bigger than expected.

We can think better of examples. it was only a way of removing classic math macros, but this will accelerate discussions like ofMin, ofMax (good one) or pi constant for example.

I can duplicate the branch and remove examples, so we can merge the core sooner and discuss examples later.

@artificiel
Copy link
Contributor

great work, I agree with @NickHardeman that internally std:: usage should be mandatory (and any type conversion should be considered a suspicious operation) (and NB if we're about cleaning up, all C-style casts should move to static_cast<T>()).

also, what about deprecating ofDegToRad & al.?

about the ofMin thing, I think it is an interesting discussion in regards to expected C++ literacy of end-users. looking at the long-term code maintenance considerations of reimplementing available functionality (which includes documentation) I would not recommend duplicating anything available in std or glm. Unless there is a good "life simplifying" macro-ish function, but in this case, while the hidden casting inside ofMin(whichever, whatever) makes things simple and generally works as expected, it's also risky. the correct way user-way would be to do auto m = std::min<float>(float, double);, and if one writes auto m = std::min(float, double) it's time to bring typing in the discussion (in that particular case it's unfortunate that the compiler error message is not more helpful). having that discussion takes more time than using ofMin() that 'just works', but it's definitely worth it.

moreover can you can use std::min on any type that has an '<' operator; you can do std::min({5,3,4,1}); the function is documented in cppreference etc; and it's an entry point into <algorithm>.

if we really want to provide ofMin(Ta, Tb), it should be templated to do the right thing most of the time (not cast down doubles, for example), so if Ta and Tb are the same, call std::min 'as is', and if not, figure out which type has the lowest std::numeric_limits::lowest() and use that type as the operation and return type. but it adds type uncertainty, and I'm not sure the maintenance effort is worth it vs empowering the user.

and/or upgrading the OF "default" type to double? so less mismatches with typing "3.14" and down casting becomes mostly moot (edge cases will still exist).

NB speaking of double, I personally work with Rust-inspired typedefs such as i32, i8, u32, f16, which makes things much more mentally clear than the old words (especially double — how braindead is that type name...long long also hurts a bit), without the visual weight of std::uint16_t — and C++23 introduces literals for floats like f32 https://en.cppreference.com/w/cpp/language/floating_literal

@artificiel
Copy link
Contributor

also a quick question vs a PR that changes so many files: there are many PR's, many of which might not automatically merge anymore... how about auditing the current list of PR's, close the ones that have become irrelevant, discuss the pending ones, before applying such a wide change?

i am currently in a period with some available time and could do some of the auditing, but I would not embark on doing that without knowing it's part of a structured effort (i.e. need to know that the work will actually be applied).

@ofTheo
Copy link
Member

ofTheo commented May 7, 2024

@dimitre I think we'll need to keep the macros in the core for now as so many addons rely on it.
so lets have OF_USE_LEGACY_MATH_MACROS enabled for now ( once this is merged ).

I think we can have it disabled by default in future releases, but let's have a couple of releases where the new usage is in the example but the old usage is preserved.

I think in the past I was in favor of doing something like of::math::PI as a replacement.
( glm::pi<float>() is kind of a clunky replacement ).

We could possibly do something like:

namespace of::math {
    // Global constant accessible in the program
    const float PI = glm::pi<float>();
    const float HALF_PI =  glm::pi<float>() * 0.5; 
    etc 
}

And have our own versions of min, max in a similar way, but I think most functions could rely on the std:: versions.

EDIT:
apologies for closing and reopening.... fat fingers... :)

@ofTheo ofTheo closed this May 7, 2024
@ofTheo ofTheo reopened this May 7, 2024
@dimitre
Copy link
Member Author

dimitre commented May 7, 2024

@ofTheo We can keep glm::pi in core, as it doens't matter to be clunky and I can remove the examples from this PR

@dimitre
Copy link
Member Author

dimitre commented May 7, 2024

@artificiel yes good to discuss ongoing PRs.
fixing conflicts will be needed anyway merging in any order, and this PR have only function replacements

@ofTheo
Copy link
Member

ofTheo commented May 7, 2024

Thanks @dimitre - I think it's good that we update the examples too.

I think probably it's just the PI vs glm::<float>pi() thing to figure out ( and maybe that could be left as PI, unless we agree on a good approach ).

I think for now we could use std::min and std::max in the examples but in the way @NickHardeman mentioned?

@dimitre
Copy link
Member Author

dimitre commented May 7, 2024

as a personal opinion I like exposing std::min, std::max, glm functions and I'm ok even with glm::pi()
I'm influenced by the name openFrameworks, like the frameworks, opened

@dimitre
Copy link
Member Author

dimitre commented May 7, 2024

@ofTheo now OF_USE_LEGACY_MATH_MACROS is defined

@ofTheo
Copy link
Member

ofTheo commented May 7, 2024

@dimitre oh yeah, I personally use glm::<float>pi() but I can see that being a mouthful for new users.
I wish they had a glm::pi() that defaulted to float.

But yeah, maybe let's just go for it and use glm::<float>pi() in the examples - the macros will still be there for a while. :)

@dimitre
Copy link
Member Author

dimitre commented May 7, 2024

Great! we can use glm::pi() in the examples and if we start using something similar in of namespace it will be easy to just replace them again in examples

@artificiel
Copy link
Contributor

glm::pi<float>() works but as mentioned by @oxillo numbers are coming in C++20 so the future-proof "modernisation" would be to define of::numbers::pi (perhaps lifting https://github.com/llvm/llvm-project/blob/main/libcxx/include/numbers into ofNumbers?)

auto pi1 = glm::pi<float>();
//
auto pi2 = of::numbers::pi; 
//
auto pi3 = of::numbers::pi_v<float>; // NB default defined as a double; if you really need a float

@ofTheo
Copy link
Member

ofTheo commented May 7, 2024

oooh - I like that approach (edit) @artificiel

how about we do:

namespace of::numbers {
    const float pi = glm::pi<float>();
    etc 
}

for now and then later we can switch to std::numbers::pi when its available?

@artificiel
Copy link
Contributor

yes, as long as the name follows the pattern of of::numbers::pi the implementation does not matter so much!

if your point is about being float by default they could be set to that, but that reinforces OF's assumptions of float, while a naked dotted decimal in C++ source is by default implicitly a double... "upgrading" the general OF behaviour to double makes it less likely to inadvertently end up with a mixture of types, and if you want to work in floats, you can cast down from a double (but it's an explicit gesture).

(I guess I'm thinking the "default plain text" of double does not match the "implicit expectation of OF" which is float.)

that being said if we're really keen on providing user-friendly "throw it anything" wrappers like ofMin() we could support float/double/whatever:

[edit to add: fell a bit in a template hole, but this should cover all arithmetic cases including the special case of size_t / uint64_t vs signed (does not play well will common_type), and still pass through any types that support < with the same type on both sides]

if we agree on this approach I will produce the ofMax version.

template<typename T>
T ofMin(const T & t1, const T & t2) const { return std::min(t1, t2); }

template<typename T, typename Q>
auto ofMin(const T & t, const Q & q) const {
	using CommonType = typename std::common_type<T, Q>::type;
	if constexpr ((std::is_same_v<T, uint64_t> or std::is_same_v<T, size_t>) && std::is_signed_v<Q>) {
		if (q < 0) {
			return q;
		} else {
			return std::min<Q>(static_cast<Q>(t), q);
		}
	} else if constexpr ((std::is_same_v<Q, uint64_t> or std::is_same_v<Q, size_t>) && std::is_signed_v<T>) {
		if (t < 0) {
			return t;
		} else {
			return std::min<T>(t, static_cast<T>(q));
		}
	} else if constexpr (std::is_signed_v<T> && std::is_unsigned_v<Q>) {
		if (t < 0 || q > static_cast<Q>(std::numeric_limits<T>::max())) {
			return static_cast<CommonType>(t);
		} else {
			return std::min(static_cast<CommonType>(t), static_cast<CommonType>(q));
		}
	} else if constexpr (std::is_signed_v<Q> && std::is_unsigned_v<T>){
		if (q < 0 || t > static_cast<T>(std::numeric_limits<Q>::max())) {
			return static_cast<CommonType>(q);
		} else {
			return std::min(static_cast<CommonType>(t), static_cast<CommonType>(q));
		}
	} else {
		return std::min(static_cast<CommonType>(t), static_cast<CommonType>(q));
	}
}

tests

assert(ofMin(size_t(1000), size_t(4)) == 4);
assert(ofMin(float(-1000.5), size_t(4)) == -1000.5f);
assert(ofMin(int(std::numeric_limits<int>::lowest()), int(4)) == std::numeric_limits<int>::lowest());
assert(ofMin(int64_t(std::numeric_limits<int64_t>::lowest()), uint16_t(3)) == std::numeric_limits<int64_t>::lowest());
assert(ofMin(float(3), int64_t(std::numeric_limits<int64_t>::lowest())) == std::numeric_limits<int64_t>::lowest());
assert(ofMin(int16_t(std::numeric_limits<int16_t>::lowest()), int64_t(222)) == std::numeric_limits<int16_t>::lowest());
assert(ofMin(int64_t(std::numeric_limits<int64_t>::lowest()), uint16_t(13)) == std::numeric_limits<int64_t>::lowest());
assert(ofMin(uint16_t(13), int64_t(std::numeric_limits<int64_t>::lowest())) == std::numeric_limits<int64_t>::lowest());
assert(ofMin(int64_t(std::numeric_limits<int64_t>::lowest()),uint64_t(3)) == std::numeric_limits<int64_t>::lowest());
assert(ofMin(uint64_t(3), int64_t(std::numeric_limits<int64_t>::lowest())) == std::numeric_limits<int64_t>::lowest());
assert(ofMin(int(-4), uint64_t(4)) == -4);
assert(ofMin(size_t(4), int(-4)) == -4);
assert(ofMin(uint16_t(20000), int16_t(10000)) == 10000);
assert(ofMin(uint16_t(20000), int16_t(-10000)) == -10000);
assert(ofMin(int16_t(10000), uint16_t(20000)) == 10000);
assert(ofMin(int16_t(-10000), uint16_t(20000)) == -10000);

@ofTheo
Copy link
Member

ofTheo commented May 8, 2024

@dimitre - I think lets merge this.
we can do a seperate PR for of::numbers::pi etc - which should be easy to do.

@NickHardeman - it would be great to get your take on @artificiel's last comment about ofMin and ofMax - if it throws less errors than std::min / std::max it might be worth adding to the core.

@dimitre dimitre merged commit 37b6178 into openframeworks:master May 8, 2024
11 checks passed
@artificiel
Copy link
Contributor

ofMin moved to #7940
of::numbers in #7941

@NickHardeman
Copy link
Contributor

@artificiel yes! ofMin() solves the issue of passing in mixed arguments that provide an easier interface and avoids the std::min( 1.f, 2.0 ); ERROR that I think could be frustrating for beginners. And std::min() is still an option for people who want to use that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants